aboutsummaryrefslogtreecommitdiff
path: root/ext/bg/js
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2020-01-04 11:54:54 -0800
committerAlex Yatskov <alex@foosoft.net>2020-01-04 11:54:54 -0800
commit2a12036ca305044291f1f4105d6a8d249848b210 (patch)
tree5cfd4a3d837bf99730233a805d72395c8c61fc07 /ext/bg/js
parent9105cb5618cfdd14c2bc37cd22db2b360fe8cd52 (diff)
parent174b92366577b0a638003b15e2d73fdc91cd62c3 (diff)
Merge branch 'master' into testing
Diffstat (limited to 'ext/bg/js')
-rw-r--r--ext/bg/js/anki.js4
-rw-r--r--ext/bg/js/api.js498
-rw-r--r--ext/bg/js/audio.js43
-rw-r--r--ext/bg/js/backend-api-forwarder.js4
-rw-r--r--ext/bg/js/backend.js614
-rw-r--r--ext/bg/js/conditions.js4
-rw-r--r--ext/bg/js/context.js4
-rw-r--r--ext/bg/js/database.js57
-rw-r--r--ext/bg/js/deinflector.js4
-rw-r--r--ext/bg/js/dictionary.js12
-rw-r--r--ext/bg/js/handlebars.js15
-rw-r--r--ext/bg/js/json-schema.js428
-rw-r--r--ext/bg/js/mecab.js4
-rw-r--r--ext/bg/js/options.js32
-rw-r--r--ext/bg/js/page-exit-prevention.js4
-rw-r--r--ext/bg/js/profile-conditions.js4
-rw-r--r--ext/bg/js/request.js4
-rw-r--r--ext/bg/js/search-frontend.js8
-rw-r--r--ext/bg/js/search-query-parser.js15
-rw-r--r--ext/bg/js/search.js144
-rw-r--r--ext/bg/js/settings/anki-templates.js40
-rw-r--r--ext/bg/js/settings/anki.js6
-rw-r--r--ext/bg/js/settings/audio-ui.js (renamed from ext/bg/js/audio-ui.js)44
-rw-r--r--ext/bg/js/settings/audio.js52
-rw-r--r--ext/bg/js/settings/backup.js370
-rw-r--r--ext/bg/js/settings/conditions-ui.js (renamed from ext/bg/js/conditions-ui.js)4
-rw-r--r--ext/bg/js/settings/dictionaries.js57
-rw-r--r--ext/bg/js/settings/main.js40
-rw-r--r--ext/bg/js/settings/popup-preview-frame.js47
-rw-r--r--ext/bg/js/settings/popup-preview.js4
-rw-r--r--ext/bg/js/settings/profiles.js20
-rw-r--r--ext/bg/js/settings/storage.js4
-rw-r--r--ext/bg/js/templates.js8
-rw-r--r--ext/bg/js/translator.js12
-rw-r--r--ext/bg/js/util.js105
35 files changed, 1905 insertions, 810 deletions
diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js
index 17b93620..10a07061 100644
--- a/ext/bg/js/anki.js
+++ b/ext/bg/js/anki.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/api.js b/ext/bg/js/api.js
index b489b8d2..906aaa30 100644
--- a/ext/bg/js/api.js
+++ b/ext/bg/js/api.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,491 +13,39 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-function apiOptionsGet(optionsContext) {
- return utilBackend().getOptions(optionsContext);
+function apiTemplateRender(template, data, dynamic) {
+ return _apiInvoke('templateRender', {data, template, dynamic});
}
-async function apiOptionsSet(changedOptions, optionsContext, source) {
- const options = await apiOptionsGet(optionsContext);
-
- function getValuePaths(obj) {
- const valuePaths = [];
- const nodes = [{obj, path: []}];
- while (nodes.length > 0) {
- const node = nodes.pop();
- for (const key of Object.keys(node.obj)) {
- const path = node.path.concat(key);
- const obj = node.obj[key];
- if (obj !== null && typeof obj === 'object') {
- nodes.unshift({obj, path});
- } else {
- valuePaths.push([obj, path]);
- }
- }
- }
- return valuePaths;
- }
-
- function modifyOption(path, value, options) {
- let pivot = options;
- for (const key of path.slice(0, -1)) {
- if (!hasOwn(pivot, key)) {
- return false;
- }
- pivot = pivot[key];
- }
- pivot[path[path.length - 1]] = value;
- return true;
- }
-
- for (const [value, path] of getValuePaths(changedOptions)) {
- modifyOption(path, value, options);
- }
-
- await apiOptionsSave(source);
-}
-
-function apiOptionsGetFull() {
- return utilBackend().getFullOptions();
-}
-
-async function apiOptionsSave(source) {
- const backend = utilBackend();
- const options = await apiOptionsGetFull();
- await optionsSave(options);
- backend.onOptionsUpdated(source);
-}
-
-async function apiTermsFind(text, details, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const [definitions, length] = await utilBackend().translator.findTerms(text, details, options);
- definitions.splice(options.general.maxResults);
- return {length, definitions};
+function apiAudioGetUrl(definition, source, optionsContext) {
+ return _apiInvoke('audioGetUrl', {definition, source, optionsContext});
}
-async function apiTextParse(text, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const translator = utilBackend().translator;
-
- const results = [];
- while (text.length > 0) {
- const term = [];
- const [definitions, sourceLength] = await translator.findTermsInternal(
- text.slice(0, options.scanning.length),
- dictEnabledSet(options),
- options.scanning.alphanumeric,
- {}
- );
- if (definitions.length > 0) {
- dictTermsSort(definitions);
- const {expression, reading} = definitions[0];
- const source = text.slice(0, sourceLength);
- for (const {text, furigana} of jpDistributeFuriganaInflected(expression, reading, source)) {
- const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
- term.push({text, reading});
- }
- text = text.slice(source.length);
- } else {
- const reading = jpConvertReading(text[0], null, options.parsing.readingMode);
- term.push({text: text[0], reading});
- text = text.slice(1);
- }
- results.push(term);
- }
- return results;
-}
-
-async function apiTextParseMecab(text, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const mecab = utilBackend().mecab;
-
- const results = {};
- const rawResults = await mecab.parseText(text);
- for (const mecabName in rawResults) {
- const result = [];
- for (const parsedLine of rawResults[mecabName]) {
- for (const {expression, reading, source} of parsedLine) {
- const term = [];
- if (expression !== null && reading !== null) {
- for (const {text, furigana} of jpDistributeFuriganaInflected(
- expression,
- jpKatakanaToHiragana(reading),
- source
- )) {
- const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
- term.push({text, reading});
+function _apiInvoke(action, params={}) {
+ const data = {action, params};
+ return new Promise((resolve, reject) => {
+ try {
+ const callback = (response) => {
+ if (response !== null && typeof response === 'object') {
+ if (typeof response.error !== 'undefined') {
+ reject(jsonToError(response.error));
+ } else {
+ resolve(response.result);
}
} else {
- const reading = jpConvertReading(source, null, options.parsing.readingMode);
- term.push({text: source, reading});
- }
- result.push(term);
- }
- result.push([{text: '\n'}]);
- }
- results[mecabName] = result;
- }
- return results;
-}
-
-async function apiKanjiFind(text, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const definitions = await utilBackend().translator.findKanji(text, options);
- definitions.splice(options.general.maxResults);
- return definitions;
-}
-
-async function apiDefinitionAdd(definition, mode, context, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
-
- if (mode !== 'kanji') {
- await audioInject(
- definition,
- options.anki.terms.fields,
- options.audio.sources,
- optionsContext
- );
- }
-
- if (context && context.screenshot) {
- await apiInjectScreenshot(
- definition,
- options.anki.terms.fields,
- context.screenshot
- );
- }
-
- const note = await dictNoteFormat(definition, mode, options);
- return utilBackend().anki.addNote(note);
-}
-
-async function apiDefinitionsAddable(definitions, modes, optionsContext) {
- const options = await apiOptionsGet(optionsContext);
- const states = [];
-
- try {
- const notes = [];
- for (const definition of definitions) {
- for (const mode of modes) {
- const note = await dictNoteFormat(definition, mode, options);
- notes.push(note);
- }
- }
-
- const cannotAdd = [];
- const anki = utilBackend().anki;
- const results = await anki.canAddNotes(notes);
- for (let resultBase = 0; resultBase < results.length; resultBase += modes.length) {
- const state = {};
- for (let modeOffset = 0; modeOffset < modes.length; ++modeOffset) {
- const index = resultBase + modeOffset;
- const result = results[index];
- const info = {canAdd: result};
- state[modes[modeOffset]] = info;
- if (!result) {
- cannotAdd.push([notes[index], info]);
- }
- }
-
- states.push(state);
- }
-
- if (cannotAdd.length > 0) {
- const noteIdsArray = await anki.findNoteIds(cannotAdd.map((e) => e[0]));
- for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) {
- const noteIds = noteIdsArray[i];
- if (noteIds.length > 0) {
- cannotAdd[i][1].noteId = noteIds[0];
+ const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`;
+ reject(new Error(`${message} (${JSON.stringify(data)})`));
}
- }
- }
- } catch (e) {
- // NOP
- }
-
- return states;
-}
-
-async function apiNoteView(noteId) {
- return utilBackend().anki.guiBrowse(`nid:${noteId}`);
-}
-
-async function apiTemplateRender(template, data, dynamic) {
- if (dynamic) {
- return handlebarsRenderDynamic(template, data);
- } else {
- return handlebarsRenderStatic(template, data);
- }
-}
-
-async function apiCommandExec(command, params) {
- const handlers = apiCommandExec.handlers;
- if (hasOwn(handlers, command)) {
- const handler = handlers[command];
- handler(params);
- }
-}
-apiCommandExec.handlers = {
- search: async (params) => {
- const url = chrome.runtime.getURL('/bg/search.html');
- if (!(params && params.newTab)) {
- try {
- const tab = await apiFindTab(1000, (url2) => (
- url2 !== null &&
- url2.startsWith(url) &&
- (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#')
- ));
- if (tab !== null) {
- await apiFocusTab(tab);
- return;
- }
- } catch (e) {
- // NOP
- }
- }
- chrome.tabs.create({url});
- },
-
- help: () => {
- chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'});
- },
-
- options: (params) => {
- if (!(params && params.newTab)) {
- chrome.runtime.openOptionsPage();
- } else {
- const manifest = chrome.runtime.getManifest();
- const url = chrome.runtime.getURL(manifest.options_ui.page);
- chrome.tabs.create({url});
- }
- },
-
- toggle: async () => {
- const optionsContext = {
- depth: 0,
- url: window.location.href
- };
- const options = await apiOptionsGet(optionsContext);
- options.general.enable = !options.general.enable;
- await apiOptionsSave('popup');
- }
-};
-
-async function apiAudioGetUrl(definition, source, optionsContext) {
- return audioGetUrl(definition, source, optionsContext);
-}
-
-async function apiInjectScreenshot(definition, fields, screenshot) {
- let usesScreenshot = false;
- for (const name in fields) {
- if (fields[name].includes('{screenshot}')) {
- usesScreenshot = true;
- break;
- }
- }
-
- if (!usesScreenshot) {
- return;
- }
-
- const dateToString = (date) => {
- const year = date.getUTCFullYear();
- const month = date.getUTCMonth().toString().padStart(2, '0');
- const day = date.getUTCDate().toString().padStart(2, '0');
- const hours = date.getUTCHours().toString().padStart(2, '0');
- const minutes = date.getUTCMinutes().toString().padStart(2, '0');
- const seconds = date.getUTCSeconds().toString().padStart(2, '0');
- return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
- };
-
- const now = new Date(Date.now());
- const filename = `yomichan_browser_screenshot_${definition.reading}_${dateToString(now)}.${screenshot.format}`;
- const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, '');
-
- try {
- await utilBackend().anki.storeMediaFile(filename, data);
- } catch (e) {
- return;
- }
-
- definition.screenshotFileName = filename;
-}
-
-function apiScreenshotGet(options, sender) {
- if (!(sender && sender.tab)) {
- return Promise.resolve();
- }
-
- const windowId = sender.tab.windowId;
- return new Promise((resolve) => {
- chrome.tabs.captureVisibleTab(windowId, options, (dataUrl) => resolve(dataUrl));
- });
-}
-
-function apiForward(action, params, sender) {
- if (!(sender && sender.tab)) {
- return Promise.resolve();
- }
-
- const tabId = sender.tab.id;
- return new Promise((resolve) => {
- chrome.tabs.sendMessage(tabId, {action, params}, (response) => resolve(response));
- });
-}
-
-function apiFrameInformationGet(sender) {
- const frameId = sender.frameId;
- return Promise.resolve({frameId});
-}
-
-function apiInjectStylesheet(css, sender) {
- if (!sender.tab) {
- return Promise.reject(new Error('Invalid tab'));
- }
-
- const tabId = sender.tab.id;
- const frameId = sender.frameId;
- const details = {
- code: css,
- runAt: 'document_start',
- cssOrigin: 'user',
- allFrames: false
- };
- if (typeof frameId === 'number') {
- details.frameId = frameId;
- }
-
- return new Promise((resolve, reject) => {
- chrome.tabs.insertCSS(tabId, details, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- });
-}
-
-async function apiGetEnvironmentInfo() {
- const browser = await apiGetBrowser();
- const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
- return {
- browser,
- platform: {
- os: platform.os
- }
- };
-}
-
-async function apiGetBrowser() {
- if (EXTENSION_IS_BROWSER_EDGE) {
- return 'edge';
- }
- if (typeof browser !== 'undefined') {
- try {
- const info = await browser.runtime.getBrowserInfo();
- if (info.name === 'Fennec') {
- return 'firefox-mobile';
- }
+ };
+ const backend = window.yomichanBackend;
+ backend.onMessage({action, params}, null, callback);
} catch (e) {
- // NOP
+ reject(e);
+ yomichan.triggerOrphaned(e);
}
- return 'firefox';
- } else {
- return 'chrome';
- }
-}
-
-function apiGetTabUrl(tab) {
- return new Promise((resolve) => {
- chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => {
- let url = null;
- if (!chrome.runtime.lastError) {
- url = (response !== null && typeof response === 'object' && !Array.isArray(response) ? response.url : null);
- if (url !== null && typeof url !== 'string') {
- url = null;
- }
- }
- resolve({tab, url});
- });
});
}
-
-async function apiFindTab(timeout, checkUrl) {
- // This function works around the need to have the "tabs" permission to access tab.url.
- const tabs = await new Promise((resolve) => chrome.tabs.query({}, resolve));
- let matchPromiseResolve = null;
- const matchPromise = new Promise((resolve) => { matchPromiseResolve = resolve; });
-
- const checkTabUrl = ({tab, url}) => {
- if (checkUrl(url, tab)) {
- matchPromiseResolve(tab);
- }
- };
-
- const promises = [];
- for (const tab of tabs) {
- const promise = apiGetTabUrl(tab);
- promise.then(checkTabUrl);
- promises.push(promise);
- }
-
- const racePromises = [
- matchPromise,
- Promise.all(promises).then(() => null)
- ];
- if (typeof timeout === 'number') {
- racePromises.push(new Promise((resolve) => setTimeout(() => resolve(null), timeout)));
- }
-
- return await Promise.race(racePromises);
-}
-
-async function apiFocusTab(tab) {
- await new Promise((resolve, reject) => {
- chrome.tabs.update(tab.id, {active: true}, () => {
- const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(); }
- });
- });
-
- if (!(typeof chrome.windows === 'object' && chrome.windows !== null)) {
- // Windows not supported (e.g. on Firefox mobile)
- return;
- }
-
- try {
- const tabWindow = await new Promise((resolve) => {
- chrome.windows.get(tab.windowId, {}, (tabWindow) => {
- const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(tabWindow); }
- });
- });
- if (!tabWindow.focused) {
- await new Promise((resolve, reject) => {
- chrome.windows.update(tab.windowId, {focused: true}, () => {
- const e = chrome.runtime.lastError;
- if (e) { reject(e); }
- else { resolve(); }
- });
- });
- }
- } catch (e) {
- // Edge throws exception for no reason here.
- }
-}
-
-async function apiClipboardGet() {
- const clipboardPasteTarget = utilBackend().clipboardPasteTarget;
- clipboardPasteTarget.innerText = '';
- clipboardPasteTarget.focus();
- document.execCommand('paste');
- return clipboardPasteTarget.innerText;
-}
diff --git a/ext/bg/js/audio.js b/ext/bg/js/audio.js
index dc0ba5eb..36ac413b 100644
--- a/ext/bg/js/audio.js
+++ b/ext/bg/js/audio.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2017-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,12 +13,12 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-const audioUrlBuilders = {
- 'jpod101': async (definition) => {
+const audioUrlBuilders = new Map([
+ ['jpod101', async (definition) => {
let kana = definition.reading;
let kanji = definition.expression;
@@ -36,8 +36,8 @@ const audioUrlBuilders = {
}
return `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
- },
- 'jpod101-alternate': async (definition) => {
+ }],
+ ['jpod101-alternate', async (definition) => {
const response = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post');
@@ -61,8 +61,8 @@ const audioUrlBuilders = {
}
throw new Error('Failed to find audio URL');
- },
- 'jisho': async (definition) => {
+ }],
+ ['jisho', async (definition) => {
const response = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', `https://jisho.org/search/${definition.expression}`);
@@ -85,37 +85,34 @@ const audioUrlBuilders = {
}
throw new Error('Failed to find audio URL');
- },
- 'text-to-speech': async (definition, optionsContext) => {
- const options = await apiOptionsGet(optionsContext);
+ }],
+ ['text-to-speech', async (definition, options) => {
const voiceURI = options.audio.textToSpeechVoice;
if (!voiceURI) {
throw new Error('No voice');
}
return `tts:?text=${encodeURIComponent(definition.expression)}&voice=${encodeURIComponent(voiceURI)}`;
- },
- 'text-to-speech-reading': async (definition, optionsContext) => {
- const options = await apiOptionsGet(optionsContext);
+ }],
+ ['text-to-speech-reading', async (definition, options) => {
const voiceURI = options.audio.textToSpeechVoice;
if (!voiceURI) {
throw new Error('No voice');
}
return `tts:?text=${encodeURIComponent(definition.reading || definition.expression)}&voice=${encodeURIComponent(voiceURI)}`;
- },
- 'custom': async (definition, optionsContext) => {
- const options = await apiOptionsGet(optionsContext);
+ }],
+ ['custom', async (definition, options) => {
const customSourceUrl = options.audio.customSourceUrl;
return customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (hasOwn(definition, m1) ? `${definition[m1]}` : m0));
- }
-};
+ }]
+]);
-async function audioGetUrl(definition, mode, optionsContext, download) {
- if (hasOwn(audioUrlBuilders, mode)) {
- const handler = audioUrlBuilders[mode];
+async function audioGetUrl(definition, mode, options, download) {
+ const handler = audioUrlBuilders.get(mode);
+ if (typeof handler === 'function') {
try {
- return await handler(definition, optionsContext, download);
+ return await handler(definition, options, download);
} catch (e) {
// NOP
}
diff --git a/ext/bg/js/backend-api-forwarder.js b/ext/bg/js/backend-api-forwarder.js
index db4d30b9..170a6b32 100644
--- a/ext/bg/js/backend-api-forwarder.js
+++ b/ext/bg/js/backend-api-forwarder.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index d9f9b586..28b0201e 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -23,6 +23,7 @@ class Backend {
this.anki = new AnkiNull();
this.mecab = new Mecab();
this.options = null;
+ this.optionsSchema = null;
this.optionsContext = {
depth: 0,
url: window.location.href
@@ -38,11 +39,20 @@ class Backend {
async prepare() {
await this.translator.prepare();
+
+ this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET');
this.options = await optionsLoad();
+ try {
+ this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, this.options);
+ } catch (e) {
+ // This shouldn't happen, but catch errors just in case of bugs
+ logError(e);
+ }
+
this.onOptionsUpdated('background');
if (chrome.commands !== null && typeof chrome.commands === 'object') {
- chrome.commands.onCommand.addListener(this.onCommand.bind(this));
+ chrome.commands.onCommand.addListener((command) => this._runCommand(command));
}
chrome.runtime.onMessage.addListener(this.onMessage.bind(this));
@@ -67,22 +77,21 @@ class Backend {
});
}
- onCommand(command) {
- apiCommandExec(command);
- }
-
onMessage({action, params}, sender, callback) {
- const handlers = Backend.messageHandlers;
- if (hasOwn(handlers, action)) {
- const handler = handlers[action];
- const promise = handler(params, sender);
+ const handler = Backend._messageHandlers.get(action);
+ if (typeof handler !== 'function') { return false; }
+
+ try {
+ const promise = handler(this, params, sender);
promise.then(
(result) => callback({result}),
(error) => callback({error: errorToJson(error)})
);
+ return true;
+ } catch (error) {
+ callback({error: errorToJson(error)});
+ return false;
}
-
- return true;
}
applyOptions() {
@@ -106,6 +115,13 @@ class Backend {
}
}
+ async getOptionsSchema() {
+ if (this.isPreparedPromise !== null) {
+ await this.isPreparedPromise;
+ }
+ return this.optionsSchema;
+ }
+
async getFullOptions() {
if (this.isPreparedPromise !== null) {
await this.isPreparedPromise;
@@ -113,6 +129,18 @@ class Backend {
return this.options;
}
+ async setFullOptions(options) {
+ if (this.isPreparedPromise !== null) {
+ await this.isPreparedPromise;
+ }
+ try {
+ this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, utilIsolate(options));
+ } catch (e) {
+ // This shouldn't happen, but catch errors just in case of bugs
+ logError(e);
+ }
+ }
+
async getOptions(optionsContext) {
if (this.isPreparedPromise !== null) {
await this.isPreparedPromise;
@@ -180,28 +208,542 @@ class Backend {
checkLastError() {
// NOP
}
+
+ _runCommand(command, params) {
+ const handler = Backend._commandHandlers.get(command);
+ if (typeof handler !== 'function') { return false; }
+
+ handler(this, params);
+ return true;
+ }
+
+ // Message handlers
+
+ _onApiOptionsSchemaGet() {
+ return this.getOptionsSchema();
+ }
+
+ _onApiOptionsGet({optionsContext}) {
+ return this.getOptions(optionsContext);
+ }
+
+ _onApiOptionsGetFull() {
+ return this.getFullOptions();
+ }
+
+ async _onApiOptionsSet({changedOptions, optionsContext, source}) {
+ const options = await this.getOptions(optionsContext);
+
+ function getValuePaths(obj) {
+ const valuePaths = [];
+ const nodes = [{obj, path: []}];
+ while (nodes.length > 0) {
+ const node = nodes.pop();
+ for (const key of Object.keys(node.obj)) {
+ const path = node.path.concat(key);
+ const obj = node.obj[key];
+ if (obj !== null && typeof obj === 'object') {
+ nodes.unshift({obj, path});
+ } else {
+ valuePaths.push([obj, path]);
+ }
+ }
+ }
+ return valuePaths;
+ }
+
+ function modifyOption(path, value, options) {
+ let pivot = options;
+ for (const key of path.slice(0, -1)) {
+ if (!hasOwn(pivot, key)) {
+ return false;
+ }
+ pivot = pivot[key];
+ }
+ pivot[path[path.length - 1]] = value;
+ return true;
+ }
+
+ for (const [value, path] of getValuePaths(changedOptions)) {
+ modifyOption(path, value, options);
+ }
+
+ await this._onApiOptionsSave({source});
+ }
+
+ async _onApiOptionsSave({source}) {
+ const options = await this.getFullOptions();
+ await optionsSave(options);
+ this.onOptionsUpdated(source);
+ }
+
+ async _onApiKanjiFind({text, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const definitions = await this.translator.findKanji(text, options);
+ definitions.splice(options.general.maxResults);
+ return definitions;
+ }
+
+ async _onApiTermsFind({text, details, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const [definitions, length] = await this.translator.findTerms(text, details, options);
+ definitions.splice(options.general.maxResults);
+ return {length, definitions};
+ }
+
+ async _onApiTextParse({text, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const results = [];
+ while (text.length > 0) {
+ const term = [];
+ const [definitions, sourceLength] = await this.translator.findTermsInternal(
+ text.substring(0, options.scanning.length),
+ dictEnabledSet(options),
+ options.scanning.alphanumeric,
+ {}
+ );
+ if (definitions.length > 0) {
+ dictTermsSort(definitions);
+ const {expression, reading} = definitions[0];
+ const source = text.substring(0, sourceLength);
+ for (const {text, furigana} of jpDistributeFuriganaInflected(expression, reading, source)) {
+ const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
+ term.push({text, reading});
+ }
+ text = text.substring(source.length);
+ } else {
+ const reading = jpConvertReading(text[0], null, options.parsing.readingMode);
+ term.push({text: text[0], reading});
+ text = text.substring(1);
+ }
+ results.push(term);
+ }
+ return results;
+ }
+
+ async _onApiTextParseMecab({text, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const results = {};
+ const rawResults = await this.mecab.parseText(text);
+ for (const mecabName in rawResults) {
+ const result = [];
+ for (const parsedLine of rawResults[mecabName]) {
+ for (const {expression, reading, source} of parsedLine) {
+ const term = [];
+ if (expression !== null && reading !== null) {
+ for (const {text, furigana} of jpDistributeFuriganaInflected(
+ expression,
+ jpKatakanaToHiragana(reading),
+ source
+ )) {
+ const reading = jpConvertReading(text, furigana, options.parsing.readingMode);
+ term.push({text, reading});
+ }
+ } else {
+ const reading = jpConvertReading(source, null, options.parsing.readingMode);
+ term.push({text: source, reading});
+ }
+ result.push(term);
+ }
+ result.push([{text: '\n'}]);
+ }
+ results[mecabName] = result;
+ }
+ return results;
+ }
+
+ async _onApiDefinitionAdd({definition, mode, context, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const templates = Backend._getTemplates(options);
+
+ if (mode !== 'kanji') {
+ await audioInject(
+ definition,
+ options.anki.terms.fields,
+ options.audio.sources,
+ optionsContext
+ );
+ }
+
+ if (context && context.screenshot) {
+ await this._injectScreenshot(
+ definition,
+ options.anki.terms.fields,
+ context.screenshot
+ );
+ }
+
+ const note = await dictNoteFormat(definition, mode, options, templates);
+ return this.anki.addNote(note);
+ }
+
+ async _onApiDefinitionsAddable({definitions, modes, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ const templates = Backend._getTemplates(options);
+ const states = [];
+
+ try {
+ const notes = [];
+ for (const definition of definitions) {
+ for (const mode of modes) {
+ const note = await dictNoteFormat(definition, mode, options, templates);
+ notes.push(note);
+ }
+ }
+
+ const cannotAdd = [];
+ const results = await this.anki.canAddNotes(notes);
+ for (let resultBase = 0; resultBase < results.length; resultBase += modes.length) {
+ const state = {};
+ for (let modeOffset = 0; modeOffset < modes.length; ++modeOffset) {
+ const index = resultBase + modeOffset;
+ const result = results[index];
+ const info = {canAdd: result};
+ state[modes[modeOffset]] = info;
+ if (!result) {
+ cannotAdd.push([notes[index], info]);
+ }
+ }
+
+ states.push(state);
+ }
+
+ if (cannotAdd.length > 0) {
+ const noteIdsArray = await this.anki.findNoteIds(cannotAdd.map((e) => e[0]));
+ for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) {
+ const noteIds = noteIdsArray[i];
+ if (noteIds.length > 0) {
+ cannotAdd[i][1].noteId = noteIds[0];
+ }
+ }
+ }
+ } catch (e) {
+ // NOP
+ }
+
+ return states;
+ }
+
+ async _onApiNoteView({noteId}) {
+ return this.anki.guiBrowse(`nid:${noteId}`);
+ }
+
+ async _onApiTemplateRender({template, data, dynamic}) {
+ return (
+ dynamic ?
+ handlebarsRenderDynamic(template, data) :
+ handlebarsRenderStatic(template, data)
+ );
+ }
+
+ async _onApiCommandExec({command, params}) {
+ return this._runCommand(command, params);
+ }
+
+ async _onApiAudioGetUrl({definition, source, optionsContext}) {
+ const options = await this.getOptions(optionsContext);
+ return await audioGetUrl(definition, source, options);
+ }
+
+ _onApiScreenshotGet({options}, sender) {
+ if (!(sender && sender.tab)) {
+ return Promise.resolve();
+ }
+
+ const windowId = sender.tab.windowId;
+ return new Promise((resolve) => {
+ chrome.tabs.captureVisibleTab(windowId, options, (dataUrl) => resolve(dataUrl));
+ });
+ }
+
+ _onApiForward({action, params}, sender) {
+ if (!(sender && sender.tab)) {
+ return Promise.resolve();
+ }
+
+ const tabId = sender.tab.id;
+ return new Promise((resolve) => {
+ chrome.tabs.sendMessage(tabId, {action, params}, (response) => resolve(response));
+ });
+ }
+
+ _onApiFrameInformationGet(params, sender) {
+ const frameId = sender.frameId;
+ return Promise.resolve({frameId});
+ }
+
+ _onApiInjectStylesheet({css}, sender) {
+ if (!sender.tab) {
+ return Promise.reject(new Error('Invalid tab'));
+ }
+
+ const tabId = sender.tab.id;
+ const frameId = sender.frameId;
+ const details = {
+ code: css,
+ runAt: 'document_start',
+ cssOrigin: 'user',
+ allFrames: false
+ };
+ if (typeof frameId === 'number') {
+ details.frameId = frameId;
+ }
+
+ return new Promise((resolve, reject) => {
+ chrome.tabs.insertCSS(tabId, details, () => {
+ const e = chrome.runtime.lastError;
+ if (e) {
+ reject(new Error(e.message));
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+
+ async _onApiGetEnvironmentInfo() {
+ const browser = await Backend._getBrowser();
+ const platform = await new Promise((resolve) => chrome.runtime.getPlatformInfo(resolve));
+ return {
+ browser,
+ platform: {
+ os: platform.os
+ }
+ };
+ }
+
+ async _onApiClipboardGet() {
+ const clipboardPasteTarget = this.clipboardPasteTarget;
+ clipboardPasteTarget.value = '';
+ clipboardPasteTarget.focus();
+ document.execCommand('paste');
+ const result = clipboardPasteTarget.value;
+ clipboardPasteTarget.value = '';
+ return result;
+ }
+
+ // Command handlers
+
+ async _onCommandSearch(params) {
+ const url = chrome.runtime.getURL('/bg/search.html');
+ if (!(params && params.newTab)) {
+ try {
+ const tab = await Backend._findTab(1000, (url2) => (
+ url2 !== null &&
+ url2.startsWith(url) &&
+ (url2.length === url.length || url2[url.length] === '?' || url2[url.length] === '#')
+ ));
+ if (tab !== null) {
+ await Backend._focusTab(tab);
+ return;
+ }
+ } catch (e) {
+ // NOP
+ }
+ }
+ chrome.tabs.create({url});
+ }
+
+ _onCommandHelp() {
+ chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'});
+ }
+
+ _onCommandOptions(params) {
+ if (!(params && params.newTab)) {
+ chrome.runtime.openOptionsPage();
+ } else {
+ const manifest = chrome.runtime.getManifest();
+ const url = chrome.runtime.getURL(manifest.options_ui.page);
+ chrome.tabs.create({url});
+ }
+ }
+
+ async _onCommandToggle() {
+ const optionsContext = {
+ depth: 0,
+ url: window.location.href
+ };
+ const source = 'popup';
+
+ const options = await this.getOptions(optionsContext);
+ options.general.enable = !options.general.enable;
+ await this._onApiOptionsSave({source});
+ }
+
+ // Utilities
+
+ async _injectScreenshot(definition, fields, screenshot) {
+ let usesScreenshot = false;
+ for (const name in fields) {
+ if (fields[name].includes('{screenshot}')) {
+ usesScreenshot = true;
+ break;
+ }
+ }
+
+ if (!usesScreenshot) {
+ return;
+ }
+
+ const dateToString = (date) => {
+ const year = date.getUTCFullYear();
+ const month = date.getUTCMonth().toString().padStart(2, '0');
+ const day = date.getUTCDate().toString().padStart(2, '0');
+ const hours = date.getUTCHours().toString().padStart(2, '0');
+ const minutes = date.getUTCMinutes().toString().padStart(2, '0');
+ const seconds = date.getUTCSeconds().toString().padStart(2, '0');
+ return `${year}-${month}-${day}-${hours}-${minutes}-${seconds}`;
+ };
+
+ const now = new Date(Date.now());
+ const filename = `yomichan_browser_screenshot_${definition.reading}_${dateToString(now)}.${screenshot.format}`;
+ const data = screenshot.dataUrl.replace(/^data:[\w\W]*?,/, '');
+
+ try {
+ await this.anki.storeMediaFile(filename, data);
+ } catch (e) {
+ return;
+ }
+
+ definition.screenshotFileName = filename;
+ }
+
+ static _getTabUrl(tab) {
+ return new Promise((resolve) => {
+ chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => {
+ let url = null;
+ if (!chrome.runtime.lastError) {
+ url = (response !== null && typeof response === 'object' && !Array.isArray(response) ? response.url : null);
+ if (url !== null && typeof url !== 'string') {
+ url = null;
+ }
+ }
+ resolve({tab, url});
+ });
+ });
+ }
+
+ static async _findTab(timeout, checkUrl) {
+ // This function works around the need to have the "tabs" permission to access tab.url.
+ const tabs = await new Promise((resolve) => chrome.tabs.query({}, resolve));
+ let matchPromiseResolve = null;
+ const matchPromise = new Promise((resolve) => { matchPromiseResolve = resolve; });
+
+ const checkTabUrl = ({tab, url}) => {
+ if (checkUrl(url, tab)) {
+ matchPromiseResolve(tab);
+ }
+ };
+
+ const promises = [];
+ for (const tab of tabs) {
+ const promise = Backend._getTabUrl(tab);
+ promise.then(checkTabUrl);
+ promises.push(promise);
+ }
+
+ const racePromises = [
+ matchPromise,
+ Promise.all(promises).then(() => null)
+ ];
+ if (typeof timeout === 'number') {
+ racePromises.push(new Promise((resolve) => setTimeout(() => resolve(null), timeout)));
+ }
+
+ return await Promise.race(racePromises);
+ }
+
+ static async _focusTab(tab) {
+ await new Promise((resolve, reject) => {
+ chrome.tabs.update(tab.id, {active: true}, () => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(); }
+ });
+ });
+
+ if (!(typeof chrome.windows === 'object' && chrome.windows !== null)) {
+ // Windows not supported (e.g. on Firefox mobile)
+ return;
+ }
+
+ try {
+ const tabWindow = await new Promise((resolve) => {
+ chrome.windows.get(tab.windowId, {}, (tabWindow) => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(tabWindow); }
+ });
+ });
+ if (!tabWindow.focused) {
+ await new Promise((resolve, reject) => {
+ chrome.windows.update(tab.windowId, {focused: true}, () => {
+ const e = chrome.runtime.lastError;
+ if (e) { reject(e); }
+ else { resolve(); }
+ });
+ });
+ }
+ } catch (e) {
+ // Edge throws exception for no reason here.
+ }
+ }
+
+ static async _getBrowser() {
+ if (EXTENSION_IS_BROWSER_EDGE) {
+ return 'edge';
+ }
+ if (typeof browser !== 'undefined') {
+ try {
+ const info = await browser.runtime.getBrowserInfo();
+ if (info.name === 'Fennec') {
+ return 'firefox-mobile';
+ }
+ } catch (e) {
+ // NOP
+ }
+ return 'firefox';
+ } else {
+ return 'chrome';
+ }
+ }
+
+ static _getTemplates(options) {
+ const templates = options.anki.fieldTemplates;
+ return typeof templates === 'string' ? templates : profileOptionsGetDefaultFieldTemplates();
+ }
}
-Backend.messageHandlers = {
- optionsGet: ({optionsContext}) => apiOptionsGet(optionsContext),
- optionsSet: ({changedOptions, optionsContext, source}) => apiOptionsSet(changedOptions, optionsContext, source),
- kanjiFind: ({text, optionsContext}) => apiKanjiFind(text, optionsContext),
- termsFind: ({text, details, optionsContext}) => apiTermsFind(text, details, optionsContext),
- textParse: ({text, optionsContext}) => apiTextParse(text, optionsContext),
- textParseMecab: ({text, optionsContext}) => apiTextParseMecab(text, optionsContext),
- definitionAdd: ({definition, mode, context, optionsContext}) => apiDefinitionAdd(definition, mode, context, optionsContext),
- definitionsAddable: ({definitions, modes, optionsContext}) => apiDefinitionsAddable(definitions, modes, optionsContext),
- noteView: ({noteId}) => apiNoteView(noteId),
- templateRender: ({template, data, dynamic}) => apiTemplateRender(template, data, dynamic),
- commandExec: ({command, params}) => apiCommandExec(command, params),
- audioGetUrl: ({definition, source, optionsContext}) => apiAudioGetUrl(definition, source, optionsContext),
- screenshotGet: ({options}, sender) => apiScreenshotGet(options, sender),
- forward: ({action, params}, sender) => apiForward(action, params, sender),
- frameInformationGet: (params, sender) => apiFrameInformationGet(sender),
- injectStylesheet: ({css}, sender) => apiInjectStylesheet(css, sender),
- getEnvironmentInfo: () => apiGetEnvironmentInfo(),
- clipboardGet: () => apiClipboardGet()
-};
-
-window.yomichan_backend = new Backend();
-window.yomichan_backend.prepare();
+Backend._messageHandlers = new Map([
+ ['optionsSchemaGet', (self, ...args) => self._onApiOptionsSchemaGet(...args)],
+ ['optionsGet', (self, ...args) => self._onApiOptionsGet(...args)],
+ ['optionsGetFull', (self, ...args) => self._onApiOptionsGetFull(...args)],
+ ['optionsSet', (self, ...args) => self._onApiOptionsSet(...args)],
+ ['optionsSave', (self, ...args) => self._onApiOptionsSave(...args)],
+ ['kanjiFind', (self, ...args) => self._onApiKanjiFind(...args)],
+ ['termsFind', (self, ...args) => self._onApiTermsFind(...args)],
+ ['textParse', (self, ...args) => self._onApiTextParse(...args)],
+ ['textParseMecab', (self, ...args) => self._onApiTextParseMecab(...args)],
+ ['definitionAdd', (self, ...args) => self._onApiDefinitionAdd(...args)],
+ ['definitionsAddable', (self, ...args) => self._onApiDefinitionsAddable(...args)],
+ ['noteView', (self, ...args) => self._onApiNoteView(...args)],
+ ['templateRender', (self, ...args) => self._onApiTemplateRender(...args)],
+ ['commandExec', (self, ...args) => self._onApiCommandExec(...args)],
+ ['audioGetUrl', (self, ...args) => self._onApiAudioGetUrl(...args)],
+ ['screenshotGet', (self, ...args) => self._onApiScreenshotGet(...args)],
+ ['forward', (self, ...args) => self._onApiForward(...args)],
+ ['frameInformationGet', (self, ...args) => self._onApiFrameInformationGet(...args)],
+ ['injectStylesheet', (self, ...args) => self._onApiInjectStylesheet(...args)],
+ ['getEnvironmentInfo', (self, ...args) => self._onApiGetEnvironmentInfo(...args)],
+ ['clipboardGet', (self, ...args) => self._onApiClipboardGet(...args)]
+]);
+
+Backend._commandHandlers = new Map([
+ ['search', (self, ...args) => self._onCommandSearch(...args)],
+ ['help', (self, ...args) => self._onCommandHelp(...args)],
+ ['options', (self, ...args) => self._onCommandOptions(...args)],
+ ['toggle', (self, ...args) => self._onCommandToggle(...args)]
+]);
+
+window.yomichanBackend = new Backend();
+window.yomichanBackend.prepare();
diff --git a/ext/bg/js/conditions.js b/ext/bg/js/conditions.js
index c0f0f301..d4d1c0e0 100644
--- a/ext/bg/js/conditions.js
+++ b/ext/bg/js/conditions.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/context.js b/ext/bg/js/context.js
index 0b21f662..834174bf 100644
--- a/ext/bg/js/context.js
+++ b/ext/bg/js/context.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2017-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
index a20d5f15..42a143f3 100644
--- a/ext/bg/js/database.js
+++ b/ext/bg/js/database.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -28,7 +28,7 @@ class Database {
}
try {
- this.db = await Database.open('dict', 4, (db, transaction, oldVersion) => {
+ this.db = await Database.open('dict', 5, (db, transaction, oldVersion) => {
Database.upgrade(db, transaction, oldVersion, [
{
version: 2,
@@ -76,6 +76,15 @@ class Database {
indices: ['dictionary', 'expression', 'reading', 'sequence']
}
}
+ },
+ {
+ version: 5,
+ stores: {
+ terms: {
+ primaryKey: {keyPath: 'id', autoIncrement: true},
+ indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse']
+ }
+ }
}
]);
});
@@ -143,14 +152,17 @@ class Database {
}
};
+ const useWildcard = !!wildcard;
+ const prefixWildcard = wildcard === 'prefix';
+
const dbTransaction = this.db.transaction(['terms'], 'readonly');
const dbTerms = dbTransaction.objectStore('terms');
- const dbIndex1 = dbTerms.index('expression');
- const dbIndex2 = dbTerms.index('reading');
+ const dbIndex1 = dbTerms.index(prefixWildcard ? 'expressionReverse' : 'expression');
+ const dbIndex2 = dbTerms.index(prefixWildcard ? 'readingReverse' : 'reading');
for (let i = 0; i < termList.length; ++i) {
- const term = termList[i];
- const query = wildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);
+ const term = prefixWildcard ? stringReverse(termList[i]) : termList[i];
+ const query = useWildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);
promises.push(
Database.getAll(dbIndex1, query, i, processRow),
Database.getAll(dbIndex2, query, i, processRow)
@@ -320,9 +332,12 @@ class Database {
return result;
}
- async importDictionary(archive, progressCallback, exceptions) {
+ async importDictionary(archive, progressCallback, details) {
this.validate();
+ const errors = [];
+ const prefixWildcardsSupported = details.prefixWildcardsSupported;
+
const maxTransactionLength = 1000;
const bulkAdd = async (objectStoreName, items, total, current) => {
const db = this.db;
@@ -337,11 +352,7 @@ class Database {
const objectStore = transaction.objectStore(objectStoreName);
await Database.bulkAdd(objectStore, items, i, count);
} catch (e) {
- if (exceptions) {
- exceptions.push(e);
- } else {
- throw e;
- }
+ errors.push(e);
}
}
};
@@ -396,6 +407,13 @@ class Database {
}
}
+ if (prefixWildcardsSupported) {
+ for (const row of rows) {
+ row.expressionReverse = stringReverse(row.expression);
+ row.readingReverse = stringReverse(row.reading);
+ }
+ }
+
await bulkAdd('terms', rows, total, current);
};
@@ -475,15 +493,18 @@ class Database {
await bulkAdd('tagMeta', rows, total, current);
};
- return await Database.importDictionaryZip(
+ const result = await Database.importDictionaryZip(
archive,
indexDataLoaded,
termDataLoaded,
termMetaDataLoaded,
kanjiDataLoaded,
kanjiMetaDataLoaded,
- tagDataLoaded
+ tagDataLoaded,
+ details
);
+
+ return {result, errors};
}
validate() {
@@ -499,7 +520,8 @@ class Database {
termMetaDataLoaded,
kanjiDataLoaded,
kanjiMetaDataLoaded,
- tagDataLoaded
+ tagDataLoaded,
+ details
) {
const zip = await JSZip.loadAsync(archive);
@@ -517,7 +539,8 @@ class Database {
title: index.title,
revision: index.revision,
sequenced: index.sequenced,
- version: index.format || index.version
+ version: index.format || index.version,
+ prefixWildcardsSupported: !!details.prefixWildcardsSupported
};
await indexDataLoaded(summary);
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
index 51f4723c..33b2a8b3 100644
--- a/ext/bg/js/deinflector.js
+++ b/ext/bg/js/deinflector.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
index 0b35e32e..92adc532 100644
--- a/ext/bg/js/dictionary.js
+++ b/ext/bg/js/dictionary.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -310,7 +310,7 @@ function dictFieldSplit(field) {
return field.length === 0 ? [] : field.split(' ');
}
-async function dictFieldFormat(field, definition, mode, options, exceptions) {
+async function dictFieldFormat(field, definition, mode, options, templates, exceptions) {
const data = {
marker: null,
definition,
@@ -329,7 +329,7 @@ async function dictFieldFormat(field, definition, mode, options, exceptions) {
}
data.marker = marker;
try {
- return await apiTemplateRender(options.anki.fieldTemplates, data, true);
+ return await apiTemplateRender(templates, data, true);
} catch (e) {
if (exceptions) { exceptions.push(e); }
return `{${marker}-render-error}`;
@@ -357,7 +357,7 @@ dictFieldFormat.markers = new Set([
'url'
]);
-async function dictNoteFormat(definition, mode, options) {
+async function dictNoteFormat(definition, mode, options, templates) {
const note = {fields: {}, tags: options.anki.tags};
let fields = [];
@@ -391,7 +391,7 @@ async function dictNoteFormat(definition, mode, options) {
}
for (const name in fields) {
- note.fields[name] = await dictFieldFormat(fields[name], definition, mode, options);
+ note.fields[name] = await dictFieldFormat(fields[name], definition, mode, options, templates);
}
return note;
diff --git a/ext/bg/js/handlebars.js b/ext/bg/js/handlebars.js
index 8f43cf9a..6d1581be 100644
--- a/ext/bg/js/handlebars.js
+++ b/ext/bg/js/handlebars.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -141,12 +141,13 @@ function handlebarsRenderStatic(name, data) {
function handlebarsRenderDynamic(template, data) {
handlebarsRegisterHelpers();
-
- Handlebars.yomichan_cache = Handlebars.yomichan_cache || {};
- let instance = Handlebars.yomichan_cache[template];
- if (!instance) {
- instance = Handlebars.yomichan_cache[template] = Handlebars.compile(template);
+ const cache = handlebarsRenderDynamic._cache;
+ let instance = cache.get(template);
+ if (typeof instance === 'undefined') {
+ instance = Handlebars.compile(template);
+ cache.set(template, instance);
}
return instance(data).trim();
}
+handlebarsRenderDynamic._cache = new Map();
diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js
new file mode 100644
index 00000000..5d596a8b
--- /dev/null
+++ b/ext/bg/js/json-schema.js
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2019-2020 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 <https://www.gnu.org/licenses/>.
+ */
+
+
+class JsonSchemaProxyHandler {
+ constructor(schema) {
+ this._schema = schema;
+ }
+
+ getPrototypeOf(target) {
+ return Object.getPrototypeOf(target);
+ }
+
+ setPrototypeOf() {
+ throw new Error('setPrototypeOf not supported');
+ }
+
+ isExtensible(target) {
+ return Object.isExtensible(target);
+ }
+
+ preventExtensions(target) {
+ Object.preventExtensions(target);
+ return true;
+ }
+
+ getOwnPropertyDescriptor(target, property) {
+ return Object.getOwnPropertyDescriptor(target, property);
+ }
+
+ defineProperty() {
+ throw new Error('defineProperty not supported');
+ }
+
+ has(target, property) {
+ return property in target;
+ }
+
+ get(target, property) {
+ if (typeof property === 'symbol') {
+ return target[property];
+ }
+
+ if (Array.isArray(target)) {
+ if (typeof property === 'string' && /^\d+$/.test(property)) {
+ property = parseInt(property, 10);
+ } else if (typeof property === 'string') {
+ return target[property];
+ }
+ }
+
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property);
+ if (propertySchema === null) {
+ return;
+ }
+
+ const value = target[property];
+ return value !== null && typeof value === 'object' ? JsonSchema.createProxy(value, propertySchema) : value;
+ }
+
+ set(target, property, value) {
+ if (Array.isArray(target)) {
+ if (typeof property === 'string' && /^\d+$/.test(property)) {
+ property = parseInt(property, 10);
+ if (property > target.length) {
+ throw new Error('Array index out of range');
+ }
+ } else if (typeof property === 'string') {
+ target[property] = value;
+ return true;
+ }
+ }
+
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(this._schema, property);
+ if (propertySchema === null) {
+ throw new Error(`Property ${property} not supported`);
+ }
+
+ value = JsonSchema.isolate(value);
+
+ const error = JsonSchemaProxyHandler.validate(value, propertySchema);
+ if (error !== null) {
+ throw new Error(`Invalid value: ${error}`);
+ }
+
+ target[property] = value;
+ return true;
+ }
+
+ deleteProperty(target, property) {
+ const required = this._schema.required;
+ if (Array.isArray(required) && required.includes(property)) {
+ throw new Error(`${property} cannot be deleted`);
+ }
+ return Reflect.deleteProperty(target, property);
+ }
+
+ ownKeys(target) {
+ return Reflect.ownKeys(target);
+ }
+
+ apply() {
+ throw new Error('apply not supported');
+ }
+
+ construct() {
+ throw new Error('construct not supported');
+ }
+
+ static getPropertySchema(schema, property) {
+ const type = schema.type;
+ if (Array.isArray(type)) {
+ throw new Error(`Ambiguous property type for ${property}`);
+ }
+ switch (type) {
+ case 'object':
+ {
+ const properties = schema.properties;
+ if (properties !== null && typeof properties === 'object' && !Array.isArray(properties)) {
+ if (Object.prototype.hasOwnProperty.call(properties, property)) {
+ return properties[property];
+ }
+ }
+
+ const additionalProperties = schema.additionalProperties;
+ return (additionalProperties !== null && typeof additionalProperties === 'object' && !Array.isArray(additionalProperties)) ? additionalProperties : null;
+ }
+ case 'array':
+ {
+ const items = schema.items;
+ return (items !== null && typeof items === 'object' && !Array.isArray(items)) ? items : null;
+ }
+ default:
+ return null;
+ }
+ }
+
+ static validate(value, schema) {
+ const type = JsonSchemaProxyHandler.getValueType(value);
+ const schemaType = schema.type;
+ if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) {
+ return `Value type ${type} does not match schema type ${schemaType}`;
+ }
+
+ const schemaEnum = schema.enum;
+ if (Array.isArray(schemaEnum) && !JsonSchemaProxyHandler.valuesAreEqualAny(value, schemaEnum)) {
+ return 'Invalid enum value';
+ }
+
+ switch (type) {
+ case 'number':
+ return JsonSchemaProxyHandler.validateNumber(value, schema);
+ case 'string':
+ return JsonSchemaProxyHandler.validateString(value, schema);
+ case 'array':
+ return JsonSchemaProxyHandler.validateArray(value, schema);
+ case 'object':
+ return JsonSchemaProxyHandler.validateObject(value, schema);
+ default:
+ return null;
+ }
+ }
+
+ static validateNumber(value, schema) {
+ const multipleOf = schema.multipleOf;
+ if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
+ return `Number is not a multiple of ${multipleOf}`;
+ }
+
+ const minimum = schema.minimum;
+ if (typeof minimum === 'number' && value < minimum) {
+ return `Number is less than ${minimum}`;
+ }
+
+ const exclusiveMinimum = schema.exclusiveMinimum;
+ if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
+ return `Number is less than or equal to ${exclusiveMinimum}`;
+ }
+
+ const maximum = schema.maximum;
+ if (typeof maximum === 'number' && value > maximum) {
+ return `Number is greater than ${maximum}`;
+ }
+
+ const exclusiveMaximum = schema.exclusiveMaximum;
+ if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
+ return `Number is greater than or equal to ${exclusiveMaximum}`;
+ }
+
+ return null;
+ }
+
+ static validateString(value, schema) {
+ const minLength = schema.minLength;
+ if (typeof minLength === 'number' && value.length < minLength) {
+ return 'String length too short';
+ }
+
+ const maxLength = schema.minLength;
+ if (typeof maxLength === 'number' && value.length > maxLength) {
+ return 'String length too long';
+ }
+
+ return null;
+ }
+
+ static validateArray(value, schema) {
+ const minItems = schema.minItems;
+ if (typeof minItems === 'number' && value.length < minItems) {
+ return 'Array length too short';
+ }
+
+ const maxItems = schema.maxItems;
+ if (typeof maxItems === 'number' && value.length > maxItems) {
+ return 'Array length too long';
+ }
+
+ return null;
+ }
+
+ static validateObject(value, schema) {
+ const properties = new Set(Object.getOwnPropertyNames(value));
+
+ const required = schema.required;
+ if (Array.isArray(required)) {
+ for (const property of required) {
+ if (!properties.has(property)) {
+ return `Missing property ${property}`;
+ }
+ }
+ }
+
+ const minProperties = schema.minProperties;
+ if (typeof minProperties === 'number' && properties.length < minProperties) {
+ return 'Not enough object properties';
+ }
+
+ const maxProperties = schema.maxProperties;
+ if (typeof maxProperties === 'number' && properties.length > maxProperties) {
+ return 'Too many object properties';
+ }
+
+ for (const property of properties) {
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ if (propertySchema === null) {
+ return `No schema found for ${property}`;
+ }
+ const error = JsonSchemaProxyHandler.validate(value[property], propertySchema);
+ if (error !== null) {
+ return error;
+ }
+ }
+
+ return null;
+ }
+
+ static isValueTypeAny(value, type, schemaTypes) {
+ if (typeof schemaTypes === 'string') {
+ return JsonSchemaProxyHandler.isValueType(value, type, schemaTypes);
+ } else if (Array.isArray(schemaTypes)) {
+ for (const schemaType of schemaTypes) {
+ if (JsonSchemaProxyHandler.isValueType(value, type, schemaType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ }
+
+ static isValueType(value, type, schemaType) {
+ return (
+ type === schemaType ||
+ (schemaType === 'integer' && Math.floor(value) === value)
+ );
+ }
+
+ static getValueType(value) {
+ const type = typeof value;
+ if (type === 'object') {
+ if (value === null) { return 'null'; }
+ if (Array.isArray(value)) { return 'array'; }
+ }
+ return type;
+ }
+
+ static valuesAreEqualAny(value1, valueList) {
+ for (const value2 of valueList) {
+ if (JsonSchemaProxyHandler.valuesAreEqual(value1, value2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static valuesAreEqual(value1, value2) {
+ return value1 === value2;
+ }
+
+ static getDefaultTypeValue(type) {
+ if (typeof type === 'string') {
+ switch (type) {
+ case 'null':
+ return null;
+ case 'boolean':
+ return false;
+ case 'number':
+ case 'integer':
+ return 0;
+ case 'string':
+ return '';
+ case 'array':
+ return [];
+ case 'object':
+ return {};
+ }
+ }
+ return null;
+ }
+
+ static getValidValueOrDefault(schema, value) {
+ let type = JsonSchemaProxyHandler.getValueType(value);
+ const schemaType = schema.type;
+ if (!JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType)) {
+ let assignDefault = true;
+
+ const schemaDefault = schema.default;
+ if (typeof schemaDefault !== 'undefined') {
+ value = JsonSchema.isolate(schemaDefault);
+ type = JsonSchemaProxyHandler.getValueType(value);
+ assignDefault = !JsonSchemaProxyHandler.isValueTypeAny(value, type, schemaType);
+ }
+
+ if (assignDefault) {
+ value = JsonSchemaProxyHandler.getDefaultTypeValue(schemaType);
+ type = JsonSchemaProxyHandler.getValueType(value);
+ }
+ }
+
+ switch (type) {
+ case 'object':
+ value = JsonSchemaProxyHandler.populateObjectDefaults(value, schema);
+ break;
+ case 'array':
+ value = JsonSchemaProxyHandler.populateArrayDefaults(value, schema);
+ break;
+ }
+
+ return value;
+ }
+
+ static populateObjectDefaults(value, schema) {
+ const properties = new Set(Object.getOwnPropertyNames(value));
+
+ const required = schema.required;
+ if (Array.isArray(required)) {
+ for (const property of required) {
+ properties.delete(property);
+
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ if (propertySchema === null) { continue; }
+ value[property] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[property]);
+ }
+ }
+
+ for (const property of properties) {
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, property);
+ if (propertySchema === null) {
+ Reflect.deleteProperty(value, property);
+ } else {
+ value[property] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[property]);
+ }
+ }
+
+ return value;
+ }
+
+ static populateArrayDefaults(value, schema) {
+ for (let i = 0, ii = value.length; i < ii; ++i) {
+ const propertySchema = JsonSchemaProxyHandler.getPropertySchema(schema, i);
+ if (propertySchema === null) { continue; }
+ value[i] = JsonSchemaProxyHandler.getValidValueOrDefault(propertySchema, value[i]);
+ }
+
+ return value;
+ }
+}
+
+class JsonSchema {
+ static createProxy(target, schema) {
+ return new Proxy(target, new JsonSchemaProxyHandler(schema));
+ }
+
+ static getValidValueOrDefault(schema, value) {
+ return JsonSchemaProxyHandler.getValidValueOrDefault(schema, value);
+ }
+
+ static isolate(value) {
+ if (value === null) { return null; }
+
+ switch (typeof value) {
+ case 'boolean':
+ case 'number':
+ case 'string':
+ case 'bigint':
+ case 'symbol':
+ return value;
+ }
+
+ const stringValue = JSON.stringify(value);
+ return typeof stringValue === 'string' ? JSON.parse(stringValue) : null;
+ }
+}
diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js
index 62111f73..8bcbb91c 100644
--- a/ext/bg/js/mecab.js
+++ b/ext/bg/js/mecab.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index e53a8a13..8021672b 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -86,6 +86,13 @@ const profileOptionsVersionUpdates = [
delete options.general.audioSource;
delete options.general.audioVolume;
delete options.general.autoPlayAudio;
+ },
+ (options) => {
+ // Version 12 changes:
+ // The preferred default value of options.anki.fieldTemplates has been changed to null.
+ if (utilStringHashCode(options.anki.fieldTemplates) === 1444379824) {
+ options.anki.fieldTemplates = null;
+ }
}
];
@@ -326,7 +333,7 @@ function profileOptionsCreateDefaults() {
screenshot: {format: 'png', quality: 92},
terms: {deck: '', model: '', fields: {}},
kanji: {deck: '', model: '', fields: {}},
- fieldTemplates: profileOptionsGetDefaultFieldTemplates()
+ fieldTemplates: null
}
};
}
@@ -378,7 +385,15 @@ function profileOptionsUpdateVersion(options) {
* ]
*/
-const optionsVersionUpdates = [];
+const optionsVersionUpdates = [
+ (options) => {
+ options.global = {
+ database: {
+ prefixWildcardsSupported: false
+ }
+ };
+ }
+];
function optionsUpdateVersion(options, defaultProfileOptions) {
// Ensure profiles is an array
@@ -423,6 +438,11 @@ function optionsUpdateVersion(options, defaultProfileOptions) {
profile.options = profileOptionsUpdateVersion(profile.options);
}
+ // Version
+ if (typeof options.version !== 'number') {
+ options.version = 0;
+ }
+
// Generic updates
return optionsGenericApplyUpdates(options, optionsVersionUpdates);
}
@@ -468,3 +488,7 @@ function optionsSave(options) {
});
});
}
+
+function optionsGetDefault() {
+ return optionsUpdateVersion({}, {});
+}
diff --git a/ext/bg/js/page-exit-prevention.js b/ext/bg/js/page-exit-prevention.js
index aee4e3c2..3a320db3 100644
--- a/ext/bg/js/page-exit-prevention.js
+++ b/ext/bg/js/page-exit-prevention.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js
index ebc6680a..1fd78e5d 100644
--- a/ext/bg/js/profile-conditions.js
+++ b/ext/bg/js/profile-conditions.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/request.js b/ext/bg/js/request.js
index 7d73d49b..b584c9a9 100644
--- a/ext/bg/js/request.js
+++ b/ext/bg/js/request.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2017-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/search-frontend.js b/ext/bg/js/search-frontend.js
index 6ba8467e..2fe50a13 100644
--- a/ext/bg/js/search-frontend.js
+++ b/ext/bg/js/search-frontend.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -33,6 +33,7 @@ async function searchFrontendSetup() {
window.frontendInitializationData = {depth: 1, ignoreNodes, proxy: false};
const scriptSrcs = [
+ '/mixed/js/text-scanner.js',
'/fg/js/frontend-api-receiver.js',
'/fg/js/popup.js',
'/fg/js/popup-proxy-host.js',
@@ -40,6 +41,9 @@ async function searchFrontendSetup() {
'/fg/js/frontend-initialize.js'
];
for (const src of scriptSrcs) {
+ const node = document.querySelector(`script[src='${src}']`);
+ if (node !== null) { continue; }
+
const script = document.createElement('script');
script.async = false;
script.src = src;
diff --git a/ext/bg/js/search-query-parser.js b/ext/bg/js/search-query-parser.js
index 8dc2e30a..0b3eccbd 100644
--- a/ext/bg/js/search-query-parser.js
+++ b/ext/bg/js/search-query-parser.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -62,7 +62,7 @@ class QueryParser {
const scanningOptions = this.search.options.scanning;
const scanningModifier = scanningOptions.modifier;
if (!(
- Frontend.isScanningModifierPressed(scanningModifier, e) ||
+ TextScanner.isScanningModifierPressed(scanningModifier, e) ||
(scanningOptions.middleMouse && DOM.isMouseButtonDown(e, 'auxiliary'))
)) {
return;
@@ -148,10 +148,9 @@ class QueryParser {
async setPreview(text) {
const previewTerms = [];
- while (text.length > 0) {
- const tempText = text.slice(0, 2);
- previewTerms.push([{text: Array.from(tempText)}]);
- text = text.slice(2);
+ for (let i = 0, ii = text.length; i < ii; i += 2) {
+ const tempText = text.substring(i, i + 2);
+ previewTerms.push([{text: tempText.split('')}]);
}
this.queryParser.innerHTML = await apiTemplateRender('query-parser.html', {
terms: previewTerms,
@@ -218,7 +217,7 @@ class QueryParser {
return result.map((term) => {
return term.filter((part) => part.text.trim()).map((part) => {
return {
- text: Array.from(part.text),
+ text: part.text.split(''),
reading: part.reading,
raw: !part.reading || !part.reading.trim()
};
diff --git a/ext/bg/js/search.js b/ext/bg/js/search.js
index fe48773f..a4103ef2 100644
--- a/ext/bg/js/search.js
+++ b/ext/bg/js/search.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,16 +13,9 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-
-let IS_FIREFOX = null;
-(async () => {
- const {browser} = await apiGetEnvironmentInfo();
- IS_FIREFOX = ['firefox', 'firefox-mobile'].includes(browser);
-})();
-
class DisplaySearch extends Display {
constructor() {
super(document.querySelector('#spinner'), document.querySelector('#content'));
@@ -43,8 +36,12 @@ class DisplaySearch extends Display {
this.introVisible = true;
this.introAnimationTimer = null;
- this.clipboardMonitorIntervalId = null;
- this.clipboardPrevText = null;
+ this.isFirefox = false;
+
+ this.clipboardMonitorTimerId = null;
+ this.clipboardMonitorTimerToken = null;
+ this.clipboardInterval = 250;
+ this.clipboardPreviousText = null;
}
static create() {
@@ -56,6 +53,7 @@ class DisplaySearch extends Display {
async prepare() {
try {
await this.initialize();
+ this.isFirefox = await DisplaySearch._isFirefox();
if (this.search !== null) {
this.search.addEventListener('click', (e) => this.onSearch(e), false);
@@ -207,10 +205,14 @@ class DisplaySearch extends Display {
async onSearchQueryUpdated(query, animate) {
try {
const details = {};
- const match = /[*\uff0a]+$/.exec(query);
+ const match = /^([*\uff0a]*)([\w\W]*?)([*\uff0a]*)$/.exec(query);
if (match !== null) {
- details.wildcard = true;
- query = query.substring(0, query.length - match[0].length);
+ if (match[1]) {
+ details.wildcard = 'prefix';
+ } else if (match[3]) {
+ details.wildcard = 'suffix';
+ }
+ query = match[2];
}
const valid = (query.length > 0);
@@ -224,63 +226,81 @@ class DisplaySearch extends Display {
sentence: {text: query, offset: 0},
url: window.location.href
});
- this.setTitleText(query);
} else {
this.container.textContent = '';
}
+ this.setTitleText(query);
window.parent.postMessage('popupClose', '*');
} catch (e) {
this.onError(e);
}
}
- onRuntimeMessage({action, params}, sender, callback) {
- const handlers = DisplaySearch.runtimeMessageHandlers;
- if (hasOwn(handlers, action)) {
- const handler = handlers[action];
- const result = handler(this, params);
- callback(result);
- } else {
- return super.onRuntimeMessage({action, params}, sender, callback);
- }
- }
-
initClipboardMonitor() {
// ignore copy from search page
window.addEventListener('copy', () => {
- this.clipboardPrevText = document.getSelection().toString().trim();
+ this.clipboardPreviousText = document.getSelection().toString().trim();
});
}
startClipboardMonitor() {
- this.clipboardMonitorIntervalId = setInterval(async () => {
- let curText = null;
- // TODO get rid of this and figure out why apiClipboardGet doesn't work on Firefox
- if (IS_FIREFOX) {
- curText = (await navigator.clipboard.readText()).trim();
- } else if (IS_FIREFOX === false) {
- curText = (await apiClipboardGet()).trim();
- }
- if (curText && (curText !== this.clipboardPrevText) && jpIsJapaneseText(curText)) {
- if (this.isWanakanaEnabled()) {
- this.setQuery(window.wanakana.toKana(curText));
- } else {
- this.setQuery(curText);
+ // The token below is used as a unique identifier to ensure that a new clipboard monitor
+ // hasn't been started during the await call. The check below the await this.getClipboardText()
+ // call will exit early if the reference has changed.
+ const token = {};
+ const intervalCallback = async () => {
+ this.clipboardMonitorTimerId = null;
+
+ let text = await this.getClipboardText();
+ if (this.clipboardMonitorTimerToken !== token) { return; }
+
+ if (
+ typeof text === 'string' &&
+ (text = text.trim()).length > 0 &&
+ text !== this.clipboardPreviousText
+ ) {
+ this.clipboardPreviousText = text;
+ if (jpIsJapaneseText(text)) {
+ this.setQuery(this.isWanakanaEnabled() ? window.wanakana.toKana(text) : text);
+ window.history.pushState(null, '', `${window.location.pathname}?query=${encodeURIComponent(text)}`);
+ this.onSearchQueryUpdated(this.query.value, true);
}
+ }
- const queryString = curText.length > 0 ? `?query=${encodeURIComponent(curText)}` : '';
- window.history.pushState(null, '', `${window.location.pathname}${queryString}`);
- this.onSearchQueryUpdated(this.query.value, true);
+ this.clipboardMonitorTimerId = setTimeout(intervalCallback, this.clipboardInterval);
+ };
- this.clipboardPrevText = curText;
- }
- }, 100);
+ this.clipboardMonitorTimerToken = token;
+
+ intervalCallback();
}
stopClipboardMonitor() {
- if (this.clipboardMonitorIntervalId) {
- clearInterval(this.clipboardMonitorIntervalId);
- this.clipboardMonitorIntervalId = null;
+ this.clipboardMonitorTimerToken = null;
+ if (this.clipboardMonitorTimerId !== null) {
+ clearTimeout(this.clipboardMonitorTimerId);
+ this.clipboardMonitorTimerId = null;
+ }
+ }
+
+ async getClipboardText() {
+ /*
+ Notes:
+ apiClipboardGet doesn't work on Firefox because document.execCommand('paste')
+ results in an empty string on the web extension background page.
+ This may be a bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1603985
+ Therefore, navigator.clipboard.readText() is used on Firefox.
+
+ navigator.clipboard.readText() can't be used in Chrome for two reasons:
+ * Requires page to be focused, else it rejects with an exception.
+ * When the page is focused, Chrome will request clipboard permission, despite already
+ being an extension with clipboard permissions. It effectively asks for the
+ non-extension permission for clipboard access.
+ */
+ try {
+ return this.isFirefox ? await navigator.clipboard.readText() : await apiClipboardGet();
+ } catch (e) {
+ return null;
}
}
@@ -360,22 +380,32 @@ class DisplaySearch extends Display {
setTitleText(text) {
// Chrome limits title to 1024 characters
if (text.length > 1000) {
- text = text.slice(0, 1000) + '...';
+ text = text.substring(0, 1000) + '...';
+ }
+
+ if (text.length === 0) {
+ document.title = 'Yomichan Search';
+ } else {
+ document.title = `${text} - Yomichan Search`;
}
- document.title = `${text} - Yomichan Search`;
}
static getSearchQueryFromLocation(url) {
const match = /^[^?#]*\?(?:[^&#]*&)?query=([^&#]*)/.exec(url);
return match !== null ? decodeURIComponent(match[1]) : null;
}
-}
-DisplaySearch.runtimeMessageHandlers = {
- getUrl: () => {
- return {url: window.location.href};
+ static async _isFirefox() {
+ const {browser} = await apiGetEnvironmentInfo();
+ switch (browser) {
+ case 'firefox':
+ case 'firefox-mobile':
+ return true;
+ default:
+ return false;
+ }
}
-};
+}
DisplaySearch.onKeyDownIgnoreKeys = {
'ANY_MOD': [
@@ -392,4 +422,4 @@ DisplaySearch.onKeyDownIgnoreKeys = {
'Shift': []
};
-window.yomichan_search = DisplaySearch.create();
+DisplaySearch.instance = DisplaySearch.create();
diff --git a/ext/bg/js/settings/anki-templates.js b/ext/bg/js/settings/anki-templates.js
index 9cdfc134..5e74358f 100644
--- a/ext/bg/js/settings/anki-templates.js
+++ b/ext/bg/js/settings/anki-templates.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -42,10 +42,22 @@ function ankiTemplatesInitialize() {
node.addEventListener('click', onAnkiTemplateMarkerClicked, false);
}
- $('#field-templates').on('change', (e) => onAnkiTemplatesValidateCompile(e));
+ $('#field-templates').on('change', (e) => onAnkiFieldTemplatesChanged(e));
$('#field-template-render').on('click', (e) => onAnkiTemplateRender(e));
$('#field-templates-reset').on('click', (e) => onAnkiFieldTemplatesReset(e));
$('#field-templates-reset-confirm').on('click', (e) => onAnkiFieldTemplatesResetConfirm(e));
+
+ ankiTemplatesUpdateValue();
+}
+
+async function ankiTemplatesUpdateValue() {
+ const optionsContext = getOptionsContext();
+ const options = await apiOptionsGet(optionsContext);
+ let templates = options.anki.fieldTemplates;
+ if (typeof templates !== 'string') { templates = profileOptionsGetDefaultFieldTemplates(); }
+ $('#field-templates').val(templates);
+
+ onAnkiTemplatesValidateCompile();
}
const ankiTemplatesValidateGetDefinition = (() => {
@@ -73,7 +85,9 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i
const definition = await ankiTemplatesValidateGetDefinition(text, optionsContext);
if (definition !== null) {
const options = await apiOptionsGet(optionsContext);
- result = await dictFieldFormat(field, definition, mode, options, exceptions);
+ let templates = options.anki.fieldTemplates;
+ if (typeof templates !== 'string') { templates = profileOptionsGetDefaultFieldTemplates(); }
+ result = await dictFieldFormat(field, definition, mode, options, templates, exceptions);
}
} catch (e) {
exceptions.push(e);
@@ -89,6 +103,24 @@ async function ankiTemplatesValidate(infoNode, field, mode, showSuccessResult, i
}
}
+async function onAnkiFieldTemplatesChanged(e) {
+ // Get value
+ let templates = e.currentTarget.value;
+ if (templates === profileOptionsGetDefaultFieldTemplates()) {
+ // Default
+ templates = null;
+ }
+
+ // Overwrite
+ const optionsContext = getOptionsContext();
+ const options = await getOptionsMutable(optionsContext);
+ options.anki.fieldTemplates = templates;
+ await settingsSaveOptions();
+
+ // Compile
+ onAnkiTemplatesValidateCompile();
+}
+
function onAnkiTemplatesValidateCompile() {
const infoNode = document.querySelector('#field-template-compile-result');
ankiTemplatesValidate(infoNode, '{expression}', 'term-kanji', false, true);
diff --git a/ext/bg/js/settings/anki.js b/ext/bg/js/settings/anki.js
index e1aabbaf..5f7989b8 100644
--- a/ext/bg/js/settings/anki.js
+++ b/ext/bg/js/settings/anki.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -154,7 +154,7 @@ async function _onAnkiModelChanged(e) {
}
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
options.anki[tabId].fields = utilBackgroundIsolate(fields);
await settingsSaveOptions();
diff --git a/ext/bg/js/audio-ui.js b/ext/bg/js/settings/audio-ui.js
index 381129ac..711c2291 100644
--- a/ext/bg/js/audio-ui.js
+++ b/ext/bg/js/settings/audio-ui.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -21,7 +21,7 @@ class AudioSourceUI {
static instantiateTemplate(templateSelector) {
const template = document.querySelector(templateSelector);
const content = document.importNode(template.content, true);
- return $(content.firstChild);
+ return content.firstChild;
}
}
@@ -32,13 +32,14 @@ AudioSourceUI.Container = class Container {
this.addButton = addButton;
this.children = [];
- this.container.empty();
+ this.container.textContent = '';
for (const audioSource of toIterable(audioSources)) {
this.children.push(new AudioSourceUI.AudioSource(this, audioSource, this.children.length));
}
- this.addButton.on('click', () => this.onAddAudioSource());
+ this._clickListener = () => this.onAddAudioSource();
+ this.addButton.addEventListener('click', this._clickListener, false);
}
cleanup() {
@@ -46,8 +47,9 @@ AudioSourceUI.Container = class Container {
child.cleanup();
}
- this.addButton.off('click');
- this.container.empty();
+ this.addButton.removeEventListener('click', this._clickListener, false);
+ this.container.textContent = '';
+ this._clickListener = null;
}
save() {
@@ -98,20 +100,28 @@ AudioSourceUI.AudioSource = class AudioSource {
this.audioSource = audioSource;
this.index = index;
- this.container = AudioSourceUI.instantiateTemplate('#audio-source-template').appendTo(parent.container);
- this.select = this.container.find('.audio-source-select');
- this.removeButton = this.container.find('.audio-source-remove');
+ this.container = AudioSourceUI.instantiateTemplate('#audio-source-template');
+ this.select = this.container.querySelector('.audio-source-select');
+ this.removeButton = this.container.querySelector('.audio-source-remove');
- this.select.val(audioSource);
+ this.select.value = audioSource;
- this.select.on('change', () => this.onSelectChanged());
- this.removeButton.on('click', () => this.onRemoveClicked());
+ this._selectChangeListener = () => this.onSelectChanged();
+ this._removeClickListener = () => this.onRemoveClicked();
+
+ this.select.addEventListener('change', this._selectChangeListener, false);
+ this.removeButton.addEventListener('click', this._removeClickListener, false);
+
+ parent.container.appendChild(this.container);
}
cleanup() {
- this.select.off('change');
- this.removeButton.off('click');
- this.container.remove();
+ this.select.removeEventListener('change', this._selectChangeListener, false);
+ this.removeButton.removeEventListener('click', this._removeClickListener, false);
+
+ if (this.container.parentNode !== null) {
+ this.container.parentNode.removeChild(this.container);
+ }
}
save() {
@@ -119,7 +129,7 @@ AudioSourceUI.AudioSource = class AudioSource {
}
onSelectChanged() {
- this.audioSource = this.select.val();
+ this.audioSource = this.select.value;
this.parent.audioSources[this.index] = this.audioSource;
this.save();
}
diff --git a/ext/bg/js/settings/audio.js b/ext/bg/js/settings/audio.js
index f63551ed..cff3f521 100644
--- a/ext/bg/js/settings/audio.js
+++ b/ext/bg/js/settings/audio.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -21,8 +21,12 @@ let audioSourceUI = null;
async function audioSettingsInitialize() {
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
- audioSourceUI = new AudioSourceUI.Container(options.audio.sources, $('.audio-source-list'), $('.audio-source-add'));
+ const options = await getOptionsMutable(optionsContext);
+ audioSourceUI = new AudioSourceUI.Container(
+ options.audio.sources,
+ document.querySelector('.audio-source-list'),
+ document.querySelector('.audio-source-add')
+ );
audioSourceUI.save = () => settingsSaveOptions();
textToSpeechInitialize();
@@ -34,24 +38,34 @@ function textToSpeechInitialize() {
speechSynthesis.addEventListener('voiceschanged', () => updateTextToSpeechVoices(), false);
updateTextToSpeechVoices();
- $('#text-to-speech-voice-test').on('click', () => textToSpeechTest());
+ document.querySelector('#text-to-speech-voice').addEventListener('change', (e) => onTextToSpeechVoiceChange(e), false);
+ document.querySelector('#text-to-speech-voice-test').addEventListener('click', () => textToSpeechTest(), false);
}
function updateTextToSpeechVoices() {
const voices = Array.prototype.map.call(speechSynthesis.getVoices(), (voice, index) => ({voice, index}));
voices.sort(textToSpeechVoiceCompare);
- if (voices.length > 0) {
- $('#text-to-speech-voice-container').css('display', '');
- }
- const select = $('#text-to-speech-voice');
- select.empty();
- select.append($('<option>').val('').text('None'));
+ document.querySelector('#text-to-speech-voice-container').hidden = (voices.length === 0);
+
+ const fragment = document.createDocumentFragment();
+
+ let option = document.createElement('option');
+ option.value = '';
+ option.textContent = 'None';
+ fragment.appendChild(option);
+
for (const {voice} of voices) {
- select.append($('<option>').val(voice.voiceURI).text(`${voice.name} (${voice.lang})`));
+ option = document.createElement('option');
+ option.value = voice.voiceURI;
+ option.textContent = `${voice.name} (${voice.lang})`;
+ fragment.appendChild(option);
}
- select.val(select.attr('data-value'));
+ const select = document.querySelector('#text-to-speech-voice');
+ select.textContent = '';
+ select.appendChild(fragment);
+ select.value = select.dataset.value;
}
function languageTagIsJapanese(languageTag) {
@@ -78,15 +92,13 @@ function textToSpeechVoiceCompare(a, b) {
if (bIsDefault) { return 1; }
}
- if (a.index < b.index) { return -1; }
- if (a.index > b.index) { return 1; }
- return 0;
+ return a.index - b.index;
}
function textToSpeechTest() {
try {
- const text = $('#text-to-speech-voice-test').attr('data-speech-text') || '';
- const voiceURI = $('#text-to-speech-voice').val();
+ const text = document.querySelector('#text-to-speech-voice-test').dataset.speechText || '';
+ const voiceURI = document.querySelector('#text-to-speech-voice').value;
const voice = audioGetTextToSpeechVoice(voiceURI);
if (voice === null) { return; }
@@ -100,3 +112,7 @@ function textToSpeechTest() {
// NOP
}
}
+
+function onTextToSpeechVoiceChange(e) {
+ e.currentTarget.dataset.value = e.currentTarget.value;
+}
diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js
new file mode 100644
index 00000000..becdc568
--- /dev/null
+++ b/ext/bg/js/settings/backup.js
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2019-2020 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. 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 <https://www.gnu.org/licenses/>.
+ */
+
+
+// Exporting
+
+let _settingsExportToken = null;
+let _settingsExportRevoke = null;
+const SETTINGS_EXPORT_CURRENT_VERSION = 0;
+
+function _getSettingsExportDateString(date, dateSeparator, dateTimeSeparator, timeSeparator, resolution) {
+ const values = [
+ date.getUTCFullYear().toString(),
+ dateSeparator,
+ (date.getUTCMonth() + 1).toString().padStart(2, '0'),
+ dateSeparator,
+ date.getUTCDate().toString().padStart(2, '0'),
+ dateTimeSeparator,
+ date.getUTCHours().toString().padStart(2, '0'),
+ timeSeparator,
+ date.getUTCMinutes().toString().padStart(2, '0'),
+ timeSeparator,
+ date.getUTCSeconds().toString().padStart(2, '0')
+ ];
+ return values.slice(0, resolution * 2 - 1).join('');
+}
+
+async function _getSettingsExportData(date) {
+ const optionsFull = await apiOptionsGetFull();
+ const environment = await apiGetEnvironmentInfo();
+
+ const fieldTemplatesDefault = profileOptionsGetDefaultFieldTemplates();
+
+ // Format options
+ for (const {options} of optionsFull.profiles) {
+ if (options.anki.fieldTemplates === fieldTemplatesDefault || !options.anki.fieldTemplates) {
+ delete options.anki.fieldTemplates; // Default
+ }
+ }
+
+ const data = {
+ version: SETTINGS_EXPORT_CURRENT_VERSION,
+ date: _getSettingsExportDateString(date, '-', ' ', ':', 6),
+ url: chrome.runtime.getURL('/'),
+ manifest: chrome.runtime.getManifest(),
+ environment,
+ userAgent: navigator.userAgent,
+ options: optionsFull
+ };
+
+ return data;
+}
+
+function _saveBlob(blob, fileName) {
+ if (typeof navigator === 'object' && typeof navigator.msSaveBlob === 'function') {
+ if (navigator.msSaveBlob(blob)) {
+ return;
+ }
+ }
+
+ const blobUrl = URL.createObjectURL(blob);
+
+ const a = document.createElement('a');
+ a.href = blobUrl;
+ a.download = fileName;
+ a.rel = 'noopener';
+ a.target = '_blank';
+
+ const revoke = () => {
+ URL.revokeObjectURL(blobUrl);
+ a.href = '';
+ _settingsExportRevoke = null;
+ };
+ _settingsExportRevoke = revoke;
+
+ a.dispatchEvent(new MouseEvent('click'));
+ setTimeout(revoke, 60000);
+}
+
+async function _onSettingsExportClick() {
+ if (_settingsExportRevoke !== null) {
+ _settingsExportRevoke();
+ _settingsExportRevoke = null;
+ }
+
+ const date = new Date(Date.now());
+
+ const token = {};
+ _settingsExportToken = token;
+ const data = await _getSettingsExportData(date);
+ if (_settingsExportToken !== token) {
+ // A new export has been started
+ return;
+ }
+ _settingsExportToken = null;
+
+ const fileName = `yomichan-settings-${_getSettingsExportDateString(date, '-', '-', '-', 6)}.json`;
+ const blob = new Blob([JSON.stringify(data, null, 4)], {type: 'application/json'});
+ _saveBlob(blob, fileName);
+}
+
+
+// Importing
+
+async function _settingsImportSetOptionsFull(optionsFull) {
+ return utilIsolate(await utilBackend().setFullOptions(
+ utilBackgroundIsolate(optionsFull)
+ ));
+}
+
+function _showSettingsImportError(error) {
+ logError(error);
+ document.querySelector('#settings-import-error-modal-message').textContent = `${error}`;
+ $('#settings-import-error-modal').modal('show');
+}
+
+async function _showSettingsImportWarnings(warnings) {
+ const modalNode = $('#settings-import-warning-modal');
+ const buttons = document.querySelectorAll('.settings-import-warning-modal-import-button');
+ const messageContainer = document.querySelector('#settings-import-warning-modal-message');
+ if (modalNode.length === 0 || buttons.length === 0 || messageContainer === null) {
+ return {result: false};
+ }
+
+ // Set message
+ const fragment = document.createDocumentFragment();
+ for (const warning of warnings) {
+ const node = document.createElement('li');
+ node.textContent = `${warning}`;
+ fragment.appendChild(node);
+ }
+ messageContainer.textContent = '';
+ messageContainer.appendChild(fragment);
+
+ // Show modal
+ modalNode.modal('show');
+
+ // Wait for modal to close
+ return new Promise((resolve) => {
+ const onButtonClick = (e) => {
+ e.preventDefault();
+ complete({
+ result: true,
+ sanitize: e.currentTarget.dataset.importSanitize === 'true'
+ });
+ modalNode.modal('hide');
+
+ };
+ const onModalHide = () => {
+ complete({result: false});
+ };
+
+ let completed = false;
+ const complete = (result) => {
+ if (completed) { return; }
+ completed = true;
+
+ modalNode.off('hide.bs.modal', onModalHide);
+ for (const button of buttons) {
+ button.removeEventListener('click', onButtonClick, false);
+ }
+
+ resolve(result);
+ };
+
+ // Hook events
+ modalNode.on('hide.bs.modal', onModalHide);
+ for (const button of buttons) {
+ button.addEventListener('click', onButtonClick, false);
+ }
+ });
+}
+
+function _isLocalhostUrl(urlString) {
+ try {
+ const url = new URL(urlString);
+ switch (url.hostname.toLowerCase()) {
+ case 'localhost':
+ case '127.0.0.1':
+ case '[::1]':
+ switch (url.protocol.toLowerCase()) {
+ case 'http:':
+ case 'https:':
+ return true;
+ }
+ break;
+ }
+ } catch (e) {
+ // NOP
+ }
+ return false;
+}
+
+function _settingsImportSanitizeProfileOptions(options, dryRun) {
+ const warnings = [];
+
+ const anki = options.anki;
+ if (isObject(anki)) {
+ const fieldTemplates = anki.fieldTemplates;
+ if (typeof fieldTemplates === 'string') {
+ warnings.push('anki.fieldTemplates contains a non-default value');
+ if (!dryRun) {
+ delete anki.fieldTemplates;
+ }
+ }
+ const server = anki.server;
+ if (typeof server === 'string' && server.length > 0 && !_isLocalhostUrl(server)) {
+ warnings.push('anki.server uses a non-localhost URL');
+ if (!dryRun) {
+ delete anki.server;
+ }
+ }
+ }
+
+ const audio = options.audio;
+ if (isObject(audio)) {
+ const customSourceUrl = audio.customSourceUrl;
+ if (typeof customSourceUrl === 'string' && customSourceUrl.length > 0 && !_isLocalhostUrl(customSourceUrl)) {
+ warnings.push('audio.customSourceUrl uses a non-localhost URL');
+ if (!dryRun) {
+ delete audio.customSourceUrl;
+ }
+ }
+ }
+
+ return warnings;
+}
+
+function _settingsImportSanitizeOptions(optionsFull, dryRun) {
+ const warnings = new Set();
+
+ const profiles = optionsFull.profiles;
+ if (Array.isArray(profiles)) {
+ for (const profile of profiles) {
+ if (!isObject(profile)) { continue; }
+ const options = profile.options;
+ if (!isObject(options)) { continue; }
+
+ const warnings2 = _settingsImportSanitizeProfileOptions(options, dryRun);
+ for (const warning of warnings2) {
+ warnings.add(warning);
+ }
+ }
+ }
+
+ return warnings;
+}
+
+function _utf8Decode(arrayBuffer) {
+ try {
+ return new TextDecoder('utf-8').decode(arrayBuffer);
+ } catch (e) {
+ const binaryString = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
+ return decodeURIComponent(escape(binaryString));
+ }
+}
+
+async function _importSettingsFile(file) {
+ const dataString = _utf8Decode(await utilReadFileArrayBuffer(file));
+ const data = JSON.parse(dataString);
+
+ // Type check
+ if (!isObject(data)) {
+ throw new Error(`Invalid data type: ${typeof data}`);
+ }
+
+ // Version check
+ const version = data.version;
+ if (!(
+ typeof version === 'number' &&
+ Number.isFinite(version) &&
+ version === Math.floor(version)
+ )) {
+ throw new Error(`Invalid version: ${version}`);
+ }
+
+ if (!(
+ version >= 0 &&
+ version <= SETTINGS_EXPORT_CURRENT_VERSION
+ )) {
+ throw new Error(`Unsupported version: ${version}`);
+ }
+
+ // Verify options exists
+ let optionsFull = data.options;
+ if (!isObject(optionsFull)) {
+ throw new Error(`Invalid options type: ${typeof optionsFull}`);
+ }
+
+ // Upgrade options
+ optionsFull = optionsUpdateVersion(optionsFull, {});
+
+ // Check for warnings
+ const sanitizationWarnings = _settingsImportSanitizeOptions(optionsFull, true);
+
+ // Show sanitization warnings
+ if (sanitizationWarnings.size > 0) {
+ const {result, sanitize} = await _showSettingsImportWarnings(sanitizationWarnings);
+ if (!result) { return; }
+
+ if (sanitize !== false) {
+ _settingsImportSanitizeOptions(optionsFull, false);
+ }
+ }
+
+ // Assign options
+ await _settingsImportSetOptionsFull(optionsFull);
+
+ // Reload settings page
+ window.location.reload();
+}
+
+function _onSettingsImportClick() {
+ document.querySelector('#settings-import-file').click();
+}
+
+function _onSettingsImportFileChange(e) {
+ const files = e.target.files;
+ if (files.length === 0) { return; }
+
+ const file = files[0];
+ e.target.value = null;
+ _importSettingsFile(file).catch(_showSettingsImportError);
+}
+
+
+// Resetting
+
+function _onSettingsResetClick() {
+ $('#settings-reset-modal').modal('show');
+}
+
+async function _onSettingsResetConfirmClick() {
+ $('#settings-reset-modal').modal('hide');
+
+ // Get default options
+ const optionsFull = optionsGetDefault();
+
+ // Assign options
+ await _settingsImportSetOptionsFull(optionsFull);
+
+ // Reload settings page
+ window.location.reload();
+}
+
+
+// Setup
+
+window.addEventListener('DOMContentLoaded', () => {
+ document.querySelector('#settings-export').addEventListener('click', _onSettingsExportClick, false);
+ document.querySelector('#settings-import').addEventListener('click', _onSettingsImportClick, false);
+ document.querySelector('#settings-import-file').addEventListener('change', _onSettingsImportFileChange, false);
+ document.querySelector('#settings-reset').addEventListener('click', _onSettingsResetClick, false);
+ document.querySelector('#settings-reset-modal-confirm').addEventListener('click', _onSettingsResetConfirmClick, false);
+}, false);
diff --git a/ext/bg/js/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js
index cc9db087..4d041451 100644
--- a/ext/bg/js/conditions-ui.js
+++ b/ext/bg/js/settings/conditions-ui.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/settings/dictionaries.js b/ext/bg/js/settings/dictionaries.js
index 065a8abc..ed171ae9 100644
--- a/ext/bg/js/settings/dictionaries.js
+++ b/ext/bg/js/settings/dictionaries.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -189,6 +189,7 @@ class SettingsDictionaryEntryUI {
this.content.querySelector('.dict-title').textContent = this.dictionaryInfo.title;
this.content.querySelector('.dict-revision').textContent = `rev.${this.dictionaryInfo.revision}`;
+ this.content.querySelector('.dict-prefix-wildcard-searches-supported').checked = !!this.dictionaryInfo.prefixWildcardsSupported;
this.applyValues();
@@ -272,7 +273,7 @@ class SettingsDictionaryEntryUI {
progress.hidden = true;
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
onDatabaseUpdated(options);
}
}
@@ -356,9 +357,10 @@ async function dictSettingsInitialize() {
document.querySelector('#dict-file-button').addEventListener('click', (e) => onDictionaryImportButtonClick(e), false);
document.querySelector('#dict-file').addEventListener('change', (e) => onDictionaryImport(e), false);
document.querySelector('#dict-main').addEventListener('change', (e) => onDictionaryMainChanged(e), false);
+ document.querySelector('#database-enable-prefix-wildcard-searches').addEventListener('change', (e) => onDatabaseEnablePrefixWildcardSearchesChanged(e), false);
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
onDictionaryOptionsChanged(options);
onDatabaseUpdated(options);
}
@@ -366,6 +368,9 @@ async function dictSettingsInitialize() {
async function onDictionaryOptionsChanged(options) {
if (dictionaryUI === null) { return; }
dictionaryUI.setOptionsDictionaries(options.dictionaries);
+
+ const optionsFull = await apiOptionsGetFull();
+ document.querySelector('#database-enable-prefix-wildcard-searches').checked = optionsFull.global.database.prefixWildcardsSupported;
}
async function onDatabaseUpdated(options) {
@@ -420,7 +425,7 @@ async function updateMainDictionarySelect(options, dictionaries) {
async function onDictionaryMainChanged(e) {
const value = e.target.value;
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
options.general.mainDictionary = value;
settingsSaveOptions();
}
@@ -526,14 +531,14 @@ async function onDictionaryPurge(e) {
dictionarySpinnerShow(true);
await utilDatabasePurge();
- for (const options of toIterable(await getOptionsArray())) {
+ for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
options.dictionaries = utilBackgroundIsolate({});
options.general.mainDictionary = '';
}
await settingsSaveOptions();
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
onDatabaseUpdated(options);
} catch (err) {
dictionaryErrorsShow([err]);
@@ -552,6 +557,9 @@ async function onDictionaryPurge(e) {
}
async function onDictionaryImport(e) {
+ const files = [...e.target.files];
+ e.target.value = null;
+
const dictFile = $('#dict-file');
const dictControls = $('#dict-importer').hide();
const dictProgress = $('#dict-import-progress').show();
@@ -572,8 +580,11 @@ async function onDictionaryImport(e) {
}
};
- const exceptions = [];
- const files = [...e.target.files];
+ const optionsFull = await apiOptionsGetFull();
+
+ const importDetails = {
+ prefixWildcardsSupported: optionsFull.global.database.prefixWildcardsSupported
+ };
for (let i = 0, ii = files.length; i < ii; ++i) {
setProgress(0.0);
@@ -582,25 +593,26 @@ async function onDictionaryImport(e) {
dictImportInfo.textContent = `(${i + 1} of ${ii})`;
}
- const summary = await utilDatabaseImport(files[i], updateProgress, exceptions);
- for (const options of toIterable(await getOptionsArray())) {
+ const {result, errors} = await utilDatabaseImport(files[i], updateProgress, importDetails);
+ for (const {options} of toIterable((await getOptionsFullMutable()).profiles)) {
const dictionaryOptions = SettingsDictionaryListUI.createDictionaryOptions();
dictionaryOptions.enabled = true;
- options.dictionaries[summary.title] = dictionaryOptions;
- if (summary.sequenced && options.general.mainDictionary === '') {
- options.general.mainDictionary = summary.title;
+ options.dictionaries[result.title] = dictionaryOptions;
+ if (result.sequenced && options.general.mainDictionary === '') {
+ options.general.mainDictionary = result.title;
}
}
await settingsSaveOptions();
- if (exceptions.length > 0) {
- exceptions.push(`Dictionary may not have been imported properly: ${exceptions.length} error${exceptions.length === 1 ? '' : 's'} reported.`);
- dictionaryErrorsShow(exceptions);
+ if (errors.length > 0) {
+ errors.push(...errors);
+ errors.push(`Dictionary may not have been imported properly: ${errors.length} error${errors.length === 1 ? '' : 's'} reported.`);
+ dictionaryErrorsShow(errors);
}
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
onDatabaseUpdated(options);
}
} catch (err) {
@@ -616,3 +628,12 @@ async function onDictionaryImport(e) {
dictProgress.hide();
}
}
+
+
+async function onDatabaseEnablePrefixWildcardSearchesChanged(e) {
+ const optionsFull = await getOptionsFullMutable();
+ const v = !!e.target.checked;
+ if (optionsFull.global.database.prefixWildcardsSupported === v) { return; }
+ optionsFull.global.database.prefixWildcardsSupported = !!e.target.checked;
+ await settingsSaveOptions();
+}
diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
index 7456e7a4..56828a15 100644
--- a/ext/bg/js/settings/main.js
+++ b/ext/bg/js/settings/main.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,12 +13,17 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-async function getOptionsArray() {
- const optionsFull = await apiOptionsGetFull();
- return optionsFull.profiles.map((profile) => profile.options);
+function getOptionsMutable(optionsContext) {
+ return utilBackend().getOptions(
+ utilBackgroundIsolate(optionsContext)
+ );
+}
+
+function getOptionsFullMutable() {
+ return utilBackend().getFullOptions();
}
async function formRead(options) {
@@ -75,7 +80,6 @@ async function formRead(options) {
options.anki.server = $('#interface-server').val();
options.anki.screenshot.format = $('#screenshot-format').val();
options.anki.screenshot.quality = parseInt($('#screenshot-quality').val(), 10);
- options.anki.fieldTemplates = $('#field-templates').val();
if (optionsAnkiEnableOld && !ankiErrorShown()) {
options.anki.terms.deck = $('#anki-terms-deck').val();
@@ -140,9 +144,8 @@ async function formWrite(options) {
$('#interface-server').val(options.anki.server);
$('#screenshot-format').val(options.anki.screenshot.format);
$('#screenshot-quality').val(options.anki.screenshot.quality);
- $('#field-templates').val(options.anki.fieldTemplates);
- onAnkiTemplatesValidateCompile();
+ await ankiTemplatesUpdateValue();
await onAnkiOptionsChanged(options);
await onDictionaryOptionsChanged(options);
@@ -161,7 +164,9 @@ function formUpdateVisibility(options) {
if (options.general.debugInfo) {
const temp = utilIsolate(options);
- temp.anki.fieldTemplates = '...';
+ if (typeof temp.anki.fieldTemplates === 'string') {
+ temp.anki.fieldTemplates = '...';
+ }
const text = JSON.stringify(temp, null, 4);
$('#debug').text(text);
}
@@ -169,7 +174,7 @@ function formUpdateVisibility(options) {
async function onFormOptionsChanged() {
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
await formRead(options);
await settingsSaveOptions();
@@ -195,21 +200,10 @@ async function onOptionsUpdate({source}) {
if (source === thisSource) { return; }
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
await formWrite(options);
}
-function onMessage({action, params}, sender, callback) {
- switch (action) {
- case 'optionsUpdate':
- onOptionsUpdate(params);
- break;
- case 'getUrl':
- callback({url: window.location.href});
- break;
- }
-}
-
function showExtensionInformation() {
const node = document.getElementById('extension-info');
@@ -233,7 +227,7 @@ async function onReady() {
storageInfoInitialize();
- chrome.runtime.onMessage.addListener(onMessage);
+ yomichan.on('optionsUpdate', onOptionsUpdate);
}
$(document).ready(() => onReady());
diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js
index 49409968..2b727cbd 100644
--- a/ext/bg/js/settings/popup-preview-frame.js
+++ b/ext/bg/js/settings/popup-preview-frame.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -24,6 +24,7 @@ class SettingsPopupPreview {
this.popupInjectOuterStylesheetOld = Popup.injectOuterStylesheet;
this.popupShown = false;
this.themeChangeTimeout = null;
+ this.textSource = null;
}
static create() {
@@ -46,16 +47,18 @@ class SettingsPopupPreview {
window.apiOptionsGet = (...args) => this.apiOptionsGet(...args);
// Overwrite frontend
- this.frontend = Frontend.create();
- window.yomichan_frontend = this.frontend;
+ const popupHost = new PopupProxyHost();
+ await popupHost.prepare();
+
+ const popup = popupHost.createPopup(null, 0);
+ popup.setChildrenSupported(false);
+
+ this.frontend = new Frontend(popup);
this.frontend.setEnabled = function () {};
this.frontend.searchClear = function () {};
- this.frontend.popup.childrenSupported = false;
- this.frontend.popup.interactive = false;
-
- await this.frontend.isPrepared();
+ await this.frontend.prepare();
// Overwrite popup
Popup.injectOuterStylesheet = (...args) => this.popupInjectOuterStylesheet(...args);
@@ -95,7 +98,7 @@ class SettingsPopupPreview {
onWindowResize() {
if (this.frontend === null) { return; }
- const textSource = this.frontend.textSourceLast;
+ const textSource = this.textSource;
if (textSource === null) { return; }
const elementRect = textSource.getRect();
@@ -105,11 +108,10 @@ class SettingsPopupPreview {
onMessage(e) {
const {action, params} = e.data;
- const handlers = SettingsPopupPreview.messageHandlers;
- if (hasOwn(handlers, action)) {
- const handler = handlers[action];
- handler(this, params);
- }
+ const handler = SettingsPopupPreview._messageHandlers.get(action);
+ if (typeof handler !== 'function') { return; }
+
+ handler(this, params);
}
onThemeDarkCheckboxChanged(node) {
@@ -160,13 +162,14 @@ class SettingsPopupPreview {
const source = new TextSourceRange(range, range.toString(), null);
try {
- await this.frontend.searchSource(source, 'script');
+ await this.frontend.onSearchSource(source, 'script');
} finally {
source.cleanup();
}
- await this.frontend.lastShowPromise;
+ this.textSource = source;
+ await this.frontend.showContentCompleted();
- if (this.frontend.popup.isVisible()) {
+ if (this.frontend.popup.isVisibleSync()) {
this.popupShown = true;
}
@@ -174,11 +177,11 @@ class SettingsPopupPreview {
}
}
-SettingsPopupPreview.messageHandlers = {
- setText: (self, {text}) => self.setText(text),
- setCustomCss: (self, {css}) => self.setCustomCss(css),
- setCustomOuterCss: (self, {css}) => self.setCustomOuterCss(css)
-};
+SettingsPopupPreview._messageHandlers = new Map([
+ ['setText', (self, {text}) => self.setText(text)],
+ ['setCustomCss', (self, {css}) => self.setCustomCss(css)],
+ ['setCustomOuterCss', (self, {css}) => self.setCustomOuterCss(css)]
+]);
SettingsPopupPreview.instance = SettingsPopupPreview.create();
diff --git a/ext/bg/js/settings/popup-preview.js b/ext/bg/js/settings/popup-preview.js
index d8579eb1..0d20471e 100644
--- a/ext/bg/js/settings/popup-preview.js
+++ b/ext/bg/js/settings/popup-preview.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/settings/profiles.js b/ext/bg/js/settings/profiles.js
index 8c218e97..c4e68b53 100644
--- a/ext/bg/js/settings/profiles.js
+++ b/ext/bg/js/settings/profiles.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
let currentProfileIndex = 0;
@@ -27,7 +27,7 @@ function getOptionsContext() {
async function profileOptionsSetup() {
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
currentProfileIndex = optionsFull.profileCurrent;
profileOptionsSetupEventListeners();
@@ -120,7 +120,7 @@ async function profileOptionsUpdateTarget(optionsFull) {
profileFormWrite(optionsFull);
const optionsContext = getOptionsContext();
- const options = await apiOptionsGet(optionsContext);
+ const options = await getOptionsMutable(optionsContext);
await formWrite(options);
}
@@ -164,13 +164,13 @@ async function onProfileOptionsChanged(e) {
return;
}
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
await profileFormRead(optionsFull);
await settingsSaveOptions();
}
async function onTargetProfileChanged() {
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
const index = tryGetIntegerValue('#profile-target', 0, optionsFull.profiles.length);
if (index === null || currentProfileIndex === index) {
return;
@@ -182,7 +182,7 @@ async function onTargetProfileChanged() {
}
async function onProfileAdd() {
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
const profile = utilBackgroundIsolate(optionsFull.profiles[currentProfileIndex]);
profile.name = profileOptionsCreateCopyName(profile.name, optionsFull.profiles, 100);
optionsFull.profiles.push(profile);
@@ -210,7 +210,7 @@ async function onProfileRemove(e) {
async function onProfileRemoveConfirm() {
$('#profile-remove-modal').modal('hide');
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
if (optionsFull.profiles.length <= 1) {
return;
}
@@ -234,7 +234,7 @@ function onProfileNameChanged() {
}
async function onProfileMove(offset) {
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
const index = currentProfileIndex + offset;
if (index < 0 || index >= optionsFull.profiles.length) {
return;
@@ -267,7 +267,7 @@ async function onProfileCopy() {
async function onProfileCopyConfirm() {
$('#profile-copy-modal').modal('hide');
- const optionsFull = await apiOptionsGetFull();
+ const optionsFull = await getOptionsFullMutable();
const index = tryGetIntegerValue('#profile-copy-source', 0, optionsFull.profiles.length);
if (index === null || index === currentProfileIndex) {
return;
diff --git a/ext/bg/js/settings/storage.js b/ext/bg/js/settings/storage.js
index 51ca6855..6c10f665 100644
--- a/ext/bg/js/settings/storage.js
+++ b/ext/bg/js/settings/storage.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2019-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
diff --git a/ext/bg/js/templates.js b/ext/bg/js/templates.js
index 9320477f..eae4e014 100644
--- a/ext/bg/js/templates.js
+++ b/ext/bg/js/templates.js
@@ -143,11 +143,11 @@ templates['kanji.html'] = template({"1":function(container,depth0,helpers,partia
},"33":function(container,depth0,helpers,partials,data) {
return "class=\"source-term\"";
},"35":function(container,depth0,helpers,partials,data) {
- return "class=\"source-term term-button-fade\"";
+ return "class=\"source-term invisible\"";
},"37":function(container,depth0,helpers,partials,data) {
return "class=\"next-term\"";
},"39":function(container,depth0,helpers,partials,data) {
- return "class=\"next-term term-button-fade\"";
+ return "class=\"next-term invisible\"";
},"41":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
@@ -491,11 +491,11 @@ templates['terms.html'] = template({"1":function(container,depth0,helpers,partia
},"67":function(container,depth0,helpers,partials,data) {
return "class=\"source-term\"";
},"69":function(container,depth0,helpers,partials,data) {
- return "class=\"source-term term-button-fade\"";
+ return "class=\"source-term invisible\"";
},"71":function(container,depth0,helpers,partials,data) {
return "class=\"next-term\"";
},"73":function(container,depth0,helpers,partials,data) {
- return "class=\"next-term term-button-fade\"";
+ return "class=\"next-term invisible\"";
},"75":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index 202014c9..7473c6ad 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -230,7 +230,7 @@ class Translator {
const titles = Object.keys(dictionaries);
const deinflections = (
details.wildcard ?
- await this.findTermWildcard(text, titles) :
+ await this.findTermWildcard(text, titles, details.wildcard) :
await this.findTermDeinflections(text, titles)
);
@@ -268,8 +268,8 @@ class Translator {
return [definitions, length];
}
- async findTermWildcard(text, titles) {
- const definitions = await this.database.findTermsBulk([text], titles, true);
+ async findTermWildcard(text, titles, wildcard) {
+ const definitions = await this.database.findTermsBulk([text], titles, wildcard);
if (definitions.length === 0) {
return [];
}
@@ -308,7 +308,7 @@ class Translator {
deinflectionArray.push(deinflection);
}
- const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles, false);
+ const definitions = await this.database.findTermsBulk(uniqueDeinflectionTerms, titles, null);
for (const definition of definitions) {
const definitionRules = Deinflector.rulesToRuleFlags(definition.rules);
diff --git a/ext/bg/js/util.js b/ext/bg/js/util.js
index 3dd5fd55..333e814b 100644
--- a/ext/bg/js/util.js
+++ b/ext/bg/js/util.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016-2017 Alex Yatskov <alex@foosoft.net>
+ * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
@@ -13,11 +13,40 @@
* 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-function utilIsolate(data) {
- return JSON.parse(JSON.stringify(data));
+function utilIsolate(value) {
+ if (value === null) { return null; }
+
+ switch (typeof value) {
+ case 'boolean':
+ case 'number':
+ case 'string':
+ case 'bigint':
+ case 'symbol':
+ return value;
+ }
+
+ const stringValue = JSON.stringify(value);
+ return typeof stringValue === 'string' ? JSON.parse(stringValue) : null;
+}
+
+function utilFunctionIsolate(func) {
+ return function (...args) {
+ try {
+ args = args.map((v) => utilIsolate(v));
+ return func.call(this, ...args);
+ } catch (e) {
+ try {
+ String(func);
+ } catch (e2) {
+ // Dead object
+ return;
+ }
+ throw e;
+ }
+ };
}
function utilBackgroundIsolate(data) {
@@ -25,6 +54,11 @@ function utilBackgroundIsolate(data) {
return backgroundPage.utilIsolate(data);
}
+function utilBackgroundFunctionIsolate(func) {
+ const backgroundPage = chrome.extension.getBackgroundPage();
+ return backgroundPage.utilFunctionIsolate(func);
+}
+
function utilSetEqual(setA, setB) {
if (setA.size !== setB.size) {
return false;
@@ -54,6 +88,8 @@ function utilSetDifference(setA, setB) {
function utilStringHashCode(string) {
let hashCode = 0;
+ if (typeof string !== 'string') { return hashCode; }
+
for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) {
hashCode = ((hashCode << 5) - hashCode) + charCode;
hashCode |= 0;
@@ -63,44 +99,52 @@ function utilStringHashCode(string) {
}
function utilBackend() {
- return chrome.extension.getBackgroundPage().yomichan_backend;
+ return chrome.extension.getBackgroundPage().yomichanBackend;
}
-function utilAnkiGetModelNames() {
- return utilBackend().anki.getModelNames();
+async function utilAnkiGetModelNames() {
+ return utilIsolate(await utilBackend().anki.getModelNames());
}
-function utilAnkiGetDeckNames() {
- return utilBackend().anki.getDeckNames();
+async function utilAnkiGetDeckNames() {
+ return utilIsolate(await utilBackend().anki.getDeckNames());
}
-function utilDatabaseGetDictionaryInfo() {
- return utilBackend().translator.database.getDictionaryInfo();
+async function utilDatabaseGetDictionaryInfo() {
+ return utilIsolate(await utilBackend().translator.database.getDictionaryInfo());
}
-function utilDatabaseGetDictionaryCounts(dictionaryNames, getTotal) {
- return utilBackend().translator.database.getDictionaryCounts(dictionaryNames, getTotal);
+async function utilDatabaseGetDictionaryCounts(dictionaryNames, getTotal) {
+ return utilIsolate(await utilBackend().translator.database.getDictionaryCounts(
+ utilBackgroundIsolate(dictionaryNames),
+ utilBackgroundIsolate(getTotal)
+ ));
}
-function utilAnkiGetModelFieldNames(modelName) {
- return utilBackend().anki.getModelFieldNames(modelName);
+async function utilAnkiGetModelFieldNames(modelName) {
+ return utilIsolate(await utilBackend().anki.getModelFieldNames(
+ utilBackgroundIsolate(modelName)
+ ));
}
-function utilDatabasePurge() {
- return utilBackend().translator.purgeDatabase();
+async function utilDatabasePurge() {
+ return utilIsolate(await utilBackend().translator.purgeDatabase());
}
-function utilDatabaseDeleteDictionary(dictionaryName, onProgress) {
- return utilBackend().translator.database.deleteDictionary(dictionaryName, onProgress);
+async function utilDatabaseDeleteDictionary(dictionaryName, onProgress) {
+ return utilIsolate(await utilBackend().translator.database.deleteDictionary(
+ utilBackgroundIsolate(dictionaryName),
+ utilBackgroundFunctionIsolate(onProgress)
+ ));
}
-async function utilDatabaseImport(data, progress, exceptions) {
- // Edge cannot read data on the background page due to the File object
- // being created from a different window. Read on the same page instead.
- if (EXTENSION_IS_BROWSER_EDGE) {
- data = await utilReadFile(data);
- }
- return utilBackend().translator.database.importDictionary(data, progress, exceptions);
+async function utilDatabaseImport(data, onProgress, details) {
+ data = await utilReadFile(data);
+ return utilIsolate(await utilBackend().translator.database.importDictionary(
+ utilBackgroundIsolate(data),
+ utilBackgroundFunctionIsolate(onProgress),
+ utilBackgroundIsolate(details)
+ ));
}
function utilReadFile(file) {
@@ -111,3 +155,12 @@ function utilReadFile(file) {
reader.readAsBinaryString(file);
});
}
+
+function utilReadFileArrayBuffer(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = () => reject(reader.error);
+ reader.readAsArrayBuffer(file);
+ });
+}