aboutsummaryrefslogtreecommitdiff
path: root/ext/bg/js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2021-02-14 11:19:54 -0500
committerGitHub <noreply@github.com>2021-02-14 11:19:54 -0500
commite419a418f6f03ef0a24330b67e7b76c5e3a7c22d (patch)
treea4c27bdfabc9280d9f6262d93d5152a58de8bc15 /ext/bg/js
parent43d1457ebfe23196348649c245dfb942a0f00a1a (diff)
Move bg/js (#1387)
* Move bg/js/anki.js to js/comm/anki.js * Move bg/js/mecab.js to js/comm/mecab.js * Move bg/js/search-main.js to js/display/search-main.js * Move bg/js/template-patcher.js to js/templates/template-patcher.js * Move bg/js/template-renderer-frame-api.js to js/templates/template-renderer-frame-api.js * Move bg/js/template-renderer-frame-main.js to js/templates/template-renderer-frame-main.js * Move bg/js/template-renderer-proxy.js to js/templates/template-renderer-proxy.js * Move bg/js/template-renderer.js to js/templates/template-renderer.js * Move bg/js/media-utility.js to js/media/media-utility.js * Move bg/js/native-simple-dom-parser.js to js/dom/native-simple-dom-parser.js * Move bg/js/simple-dom-parser.js to js/dom/simple-dom-parser.js * Move bg/js/audio-downloader.js to js/media/audio-downloader.js * Move bg/js/deinflector.js to js/language/deinflector.js * Move bg/js/backend.js to js/background/backend.js * Move bg/js/translator.js to js/language/translator.js * Move bg/js/search-display-controller.js to js/display/search-display-controller.js * Move bg/js/request-builder.js to js/background/request-builder.js * Move bg/js/text-source-map.js to js/general/text-source-map.js * Move bg/js/clipboard-reader.js to js/comm/clipboard-reader.js * Move bg/js/clipboard-monitor.js to js/comm/clipboard-monitor.js * Move bg/js/query-parser.js to js/display/query-parser.js * Move bg/js/profile-conditions.js to js/background/profile-conditions.js * Move bg/js/dictionary-database.js to js/language/dictionary-database.js * Move bg/js/dictionary-importer.js to js/language/dictionary-importer.js * Move bg/js/anki-note-builder.js to js/data/anki-note-builder.js * Move bg/js/anki-note-data.js to js/data/anki-note-data.js * Move bg/js/database.js to js/data/database.js * Move bg/js/json-schema.js to js/data/json-schema.js * Move bg/js/options.js to js/data/options-util.js * Move bg/js/background-main.js to js/background/background-main.js * Move bg/js/permissions-util.js to js/data/permissions-util.js * Move bg/js/context-main.js to js/pages/action-popup-main.js * Move bg/js/generic-page-main.js to js/pages/generic-page-main.js * Move bg/js/info-main.js to js/pages/info-main.js * Move bg/js/permissions-main.js to js/pages/permissions-main.js * Move bg/js/welcome-main.js to js/pages/welcome-main.js
Diffstat (limited to 'ext/bg/js')
-rw-r--r--ext/bg/js/anki-note-builder.js148
-rw-r--r--ext/bg/js/anki-note-data.js240
-rw-r--r--ext/bg/js/anki.js235
-rw-r--r--ext/bg/js/audio-downloader.js317
-rw-r--r--ext/bg/js/backend.js2053
-rw-r--r--ext/bg/js/background-main.js25
-rw-r--r--ext/bg/js/clipboard-monitor.js80
-rw-r--r--ext/bg/js/clipboard-reader.js169
-rw-r--r--ext/bg/js/context-main.js233
-rw-r--r--ext/bg/js/database.js327
-rw-r--r--ext/bg/js/deinflector.js96
-rw-r--r--ext/bg/js/dictionary-database.js484
-rw-r--r--ext/bg/js/dictionary-importer.js407
-rw-r--r--ext/bg/js/generic-page-main.js32
-rw-r--r--ext/bg/js/info-main.js127
-rw-r--r--ext/bg/js/json-schema.js757
-rw-r--r--ext/bg/js/mecab.js230
-rw-r--r--ext/bg/js/media-utility.js132
-rw-r--r--ext/bg/js/native-simple-dom-parser.js50
-rw-r--r--ext/bg/js/options.js739
-rw-r--r--ext/bg/js/permissions-main.js103
-rw-r--r--ext/bg/js/permissions-util.js126
-rw-r--r--ext/bg/js/profile-conditions.js276
-rw-r--r--ext/bg/js/query-parser.js232
-rw-r--r--ext/bg/js/request-builder.js266
-rw-r--r--ext/bg/js/search-display-controller.js422
-rw-r--r--ext/bg/js/search-main.js57
-rw-r--r--ext/bg/js/simple-dom-parser.js117
-rw-r--r--ext/bg/js/template-patcher.js92
-rw-r--r--ext/bg/js/template-renderer-frame-api.js78
-rw-r--r--ext/bg/js/template-renderer-frame-main.js33
-rw-r--r--ext/bg/js/template-renderer-proxy.js157
-rw-r--r--ext/bg/js/template-renderer.js416
-rw-r--r--ext/bg/js/text-source-map.js118
-rw-r--r--ext/bg/js/translator.js1397
-rw-r--r--ext/bg/js/welcome-main.js88
36 files changed, 0 insertions, 10859 deletions
diff --git a/ext/bg/js/anki-note-builder.js b/ext/bg/js/anki-note-builder.js
deleted file mode 100644
index e1399f66..00000000
--- a/ext/bg/js/anki-note-builder.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * TemplateRendererProxy
- */
-
-class AnkiNoteBuilder {
- constructor(enabled) {
- this._markerPattern = /\{([\w-]+)\}/g;
- this._templateRenderer = enabled ? new TemplateRendererProxy() : null;
- }
-
- async createNote({
- definition,
- mode,
- context,
- templates,
- deckName,
- modelName,
- fields,
- tags=[],
- injectedMedia=null,
- checkForDuplicates=true,
- duplicateScope='collection',
- resultOutputMode='split',
- glossaryLayoutMode='default',
- compactTags=false,
- errors=null
- }) {
- let duplicateScopeDeckName = null;
- let duplicateScopeCheckChildren = false;
- if (duplicateScope === 'deck-root') {
- duplicateScope = 'deck';
- duplicateScopeDeckName = this.getRootDeckName(deckName);
- duplicateScopeCheckChildren = true;
- }
-
- const data = {
- definition,
- mode,
- context,
- resultOutputMode,
- glossaryLayoutMode,
- compactTags,
- injectedMedia
- };
- const formattedFieldValuePromises = [];
- for (const [, fieldValue] of fields) {
- const formattedFieldValuePromise = this._formatField(fieldValue, data, templates, errors);
- formattedFieldValuePromises.push(formattedFieldValuePromise);
- }
-
- const formattedFieldValues = await Promise.all(formattedFieldValuePromises);
- const noteFields = {};
- for (let i = 0, ii = fields.length; i < ii; ++i) {
- const fieldName = fields[i][0];
- const formattedFieldValue = formattedFieldValues[i];
- noteFields[fieldName] = formattedFieldValue;
- }
-
- return {
- fields: noteFields,
- tags,
- deckName,
- modelName,
- options: {
- allowDuplicate: !checkForDuplicates,
- duplicateScope,
- duplicateScopeOptions: {
- deckName: duplicateScopeDeckName,
- checkChildren: duplicateScopeCheckChildren
- }
- }
- };
- }
-
- containsMarker(fields, marker) {
- marker = `{${marker}}`;
- for (const [, fieldValue] of fields) {
- if (fieldValue.includes(marker)) {
- return true;
- }
- }
- return false;
- }
-
- containsAnyMarker(field) {
- const result = this._markerPattern.test(field);
- this._markerPattern.lastIndex = 0;
- return result;
- }
-
- getRootDeckName(deckName) {
- const index = deckName.indexOf('::');
- return index >= 0 ? deckName.substring(0, index) : deckName;
- }
-
- // Private
-
- async _formatField(field, data, templates, errors=null) {
- return await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => {
- try {
- return await this._renderTemplate(templates, data, marker);
- } catch (e) {
- if (errors) {
- const error = new Error(`Template render error for {${marker}}`);
- error.data = {error: e};
- errors.push(error);
- }
- return `{${marker}-render-error}`;
- }
- });
- }
-
- async _stringReplaceAsync(str, regex, replacer) {
- let match;
- let index = 0;
- const parts = [];
- while ((match = regex.exec(str)) !== null) {
- parts.push(str.substring(index, match.index), replacer(...match, match.index, str));
- index = regex.lastIndex;
- }
- if (parts.length === 0) {
- return str;
- }
- parts.push(str.substring(index));
- return (await Promise.all(parts)).join('');
- }
-
- async _renderTemplate(template, data, marker) {
- return await this._templateRenderer.render(template, {data, marker}, 'ankiNote');
- }
-}
diff --git a/ext/bg/js/anki-note-data.js b/ext/bg/js/anki-note-data.js
deleted file mode 100644
index a7d0f9f6..00000000
--- a/ext/bg/js/anki-note-data.js
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright (C) 2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * DictionaryDataUtil
- */
-
-/**
- * This class represents the data that is exposed to the Anki template renderer.
- * The public properties and data should be backwards compatible.
- */
-class AnkiNoteData {
- constructor({
- definition,
- resultOutputMode,
- mode,
- glossaryLayoutMode,
- compactTags,
- context,
- injectedMedia=null
- }, marker) {
- this._definition = definition;
- this._resultOutputMode = resultOutputMode;
- this._mode = mode;
- this._glossaryLayoutMode = glossaryLayoutMode;
- this._compactTags = compactTags;
- this._context = context;
- this._marker = marker;
- this._injectedMedia = injectedMedia;
- this._pitches = null;
- this._pitchCount = null;
- this._uniqueExpressions = null;
- this._uniqueReadings = null;
- this._publicContext = null;
- this._cloze = null;
-
- this._prepareDefinition(definition, injectedMedia, context);
- }
-
- get marker() {
- return this._marker;
- }
-
- set marker(value) {
- this._marker = value;
- }
-
- get definition() {
- return this._definition;
- }
-
- get uniqueExpressions() {
- if (this._uniqueExpressions === null) {
- this._uniqueExpressions = this._getUniqueExpressions();
- }
- return this._uniqueExpressions;
- }
-
- get uniqueReadings() {
- if (this._uniqueReadings === null) {
- this._uniqueReadings = this._getUniqueReadings();
- }
- return this._uniqueReadings;
- }
-
- get pitches() {
- if (this._pitches === null) {
- this._pitches = DictionaryDataUtil.getPitchAccentInfos(this._definition);
- }
- return this._pitches;
- }
-
- get pitchCount() {
- if (this._pitchCount === null) {
- this._pitchCount = this.pitches.reduce((i, v) => i + v.pitches.length, 0);
- }
- return this._pitchCount;
- }
-
- get group() {
- return this._resultOutputMode === 'group';
- }
-
- get merge() {
- return this._resultOutputMode === 'merge';
- }
-
- get modeTermKanji() {
- return this._mode === 'term-kanji';
- }
-
- get modeTermKana() {
- return this._mode === 'term-kana';
- }
-
- get modeKanji() {
- return this._mode === 'kanji';
- }
-
- get compactGlossaries() {
- return this._glossaryLayoutMode === 'compact';
- }
-
- get glossaryLayoutMode() {
- return this._glossaryLayoutMode;
- }
-
- get compactTags() {
- return this._compactTags;
- }
-
- get context() {
- if (this._publicContext === null) {
- this._publicContext = this._getPublicContext();
- }
- return this._publicContext;
- }
-
- createPublic() {
- const self = this;
- return {
- get marker() { return self.marker; },
- set marker(value) { self.marker = value; },
- get definition() { return self.definition; },
- get glossaryLayoutMode() { return self.glossaryLayoutMode; },
- get compactTags() { return self.compactTags; },
- get group() { return self.group; },
- get merge() { return self.merge; },
- get modeTermKanji() { return self.modeTermKanji; },
- get modeTermKana() { return self.modeTermKana; },
- get modeKanji() { return self.modeKanji; },
- get compactGlossaries() { return self.compactGlossaries; },
- get uniqueExpressions() { return self.uniqueExpressions; },
- get uniqueReadings() { return self.uniqueReadings; },
- get pitches() { return self.pitches; },
- get pitchCount() { return self.pitchCount; },
- get context() { return self.context; }
- };
- }
-
- // Private
-
- _asObject(value) {
- return (typeof value === 'object' && value !== null ? value : {});
- }
-
- _getUniqueExpressions() {
- const results = new Set();
- const definition = this._definition;
- if (definition.type !== 'kanji') {
- for (const {expression} of definition.expressions) {
- results.add(expression);
- }
- }
- return [...results];
- }
-
- _getUniqueReadings() {
- const results = new Set();
- const definition = this._definition;
- if (definition.type !== 'kanji') {
- for (const {reading} of definition.expressions) {
- results.add(reading);
- }
- }
- return [...results];
- }
-
- _getPublicContext() {
- let {documentTitle} = this._asObject(this._context);
- if (typeof documentTitle !== 'string') { documentTitle = ''; }
-
- return {
- document: {
- title: documentTitle
- }
- };
- }
-
- _getCloze() {
- const {sentence} = this._asObject(this._context);
- let {text, offset} = this._asObject(sentence);
- if (typeof text !== 'string') { text = ''; }
- if (typeof offset !== 'number') { offset = 0; }
-
- const definition = this._definition;
- const source = definition.type === 'kanji' ? definition.character : definition.rawSource;
-
- return {
- sentence: text,
- prefix: text.substring(0, offset),
- body: text.substring(offset, offset + source.length),
- suffix: text.substring(offset + source.length)
- };
- }
-
- _getClozeCached() {
- if (this._cloze === null) {
- this._cloze = this._getCloze();
- }
- return this._cloze;
- }
-
- _prepareDefinition(definition, injectedMedia, context) {
- const {
- screenshotFileName=null,
- clipboardImageFileName=null,
- clipboardText=null,
- audioFileName=null
- } = this._asObject(injectedMedia);
-
- let {url} = this._asObject(context);
- if (typeof url !== 'string') { url = ''; }
-
- definition.screenshotFileName = screenshotFileName;
- definition.clipboardImageFileName = clipboardImageFileName;
- definition.clipboardText = clipboardText;
- definition.audioFileName = audioFileName;
- definition.url = url;
- Object.defineProperty(definition, 'cloze', {
- configurable: true,
- enumerable: true,
- get: this._getClozeCached.bind(this)
- });
- }
-}
diff --git a/ext/bg/js/anki.js b/ext/bg/js/anki.js
deleted file mode 100644
index 251e0e0c..00000000
--- a/ext/bg/js/anki.js
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright (C) 2016-2021 Yomichan Authors
- *
- * 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 AnkiConnect {
- constructor() {
- this._enabled = false;
- this._server = null;
- this._localVersion = 2;
- this._remoteVersion = 0;
- this._versionCheckPromise = null;
- }
-
- get server() {
- return this._server;
- }
-
- set server(value) {
- this._server = value;
- }
-
- get enabled() {
- return this._enabled;
- }
-
- set enabled(value) {
- this._enabled = value;
- }
-
- async isConnected() {
- try {
- await this._invoke('version');
- return true;
- } catch (e) {
- return false;
- }
- }
-
- async getVersion() {
- if (!this._enabled) { return null; }
- await this._checkVersion();
- return await this._invoke('version', {});
- }
-
- async addNote(note) {
- if (!this._enabled) { return null; }
- await this._checkVersion();
- return await this._invoke('addNote', {note});
- }
-
- async canAddNotes(notes) {
- if (!this._enabled) { return []; }
- await this._checkVersion();
- return await this._invoke('canAddNotes', {notes});
- }
-
- async getDeckNames() {
- if (!this._enabled) { return []; }
- await this._checkVersion();
- return await this._invoke('deckNames');
- }
-
- async getModelNames() {
- if (!this._enabled) { return []; }
- await this._checkVersion();
- return await this._invoke('modelNames');
- }
-
- async getModelFieldNames(modelName) {
- if (!this._enabled) { return []; }
- await this._checkVersion();
- return await this._invoke('modelFieldNames', {modelName});
- }
-
- async guiBrowse(query) {
- if (!this._enabled) { return []; }
- await this._checkVersion();
- return await this._invoke('guiBrowse', {query});
- }
-
- async guiBrowseNote(noteId) {
- return await this.guiBrowse(`nid:${noteId}`);
- }
-
- async storeMediaFile(fileName, dataBase64) {
- if (!this._enabled) {
- throw new Error('AnkiConnect not enabled');
- }
- await this._checkVersion();
- return await this._invoke('storeMediaFile', {filename: fileName, data: dataBase64});
- }
-
- async findNoteIds(notes) {
- if (!this._enabled) { return []; }
- await this._checkVersion();
- const actions = notes.map((note) => {
- let query = '';
- switch (this._getDuplicateScopeFromNote(note)) {
- case 'deck':
- query = `"deck:${this._escapeQuery(note.deckName)}" `;
- break;
- case 'deck-root':
- query = `"deck:${this._escapeQuery(this.getRootDeckName(note.deckName))}" `;
- break;
- }
- query += this._fieldsToQuery(note.fields);
- return {action: 'findNotes', params: {query}};
- });
- return await this._invoke('multi', {actions});
- }
-
- async suspendCards(cardIds) {
- if (!this._enabled) { return false; }
- await this._checkVersion();
- return await this._invoke('suspend', {cards: cardIds});
- }
-
- async findCards(query) {
- if (!this._enabled) { return []; }
- await this._checkVersion();
- return await this._invoke('findCards', {query});
- }
-
- async findCardsForNote(noteId) {
- return await this.findCards(`nid:${noteId}`);
- }
-
- getRootDeckName(deckName) {
- const index = deckName.indexOf('::');
- return index >= 0 ? deckName.substring(0, index) : deckName;
- }
-
- // Private
-
- async _checkVersion() {
- if (this._remoteVersion < this._localVersion) {
- if (this._versionCheckPromise === null) {
- const promise = this._invoke('version');
- promise
- .catch(() => {})
- .finally(() => { this._versionCheckPromise = null; });
- this._versionCheckPromise = promise;
- }
- this._remoteVersion = await this._versionCheckPromise;
- if (this._remoteVersion < this._localVersion) {
- throw new Error('Extension and plugin versions incompatible');
- }
- }
- }
-
- async _invoke(action, params) {
- let response;
- try {
- response = await fetch(this._server, {
- method: 'POST',
- mode: 'cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer',
- body: JSON.stringify({action, params, version: this._localVersion})
- });
- } catch (e) {
- const error = new Error('Anki connection failure');
- error.data = {action, params};
- throw error;
- }
-
- if (!response.ok) {
- const error = new Error(`Anki connection error: ${response.status}`);
- error.data = {action, params, status: response.status};
- throw error;
- }
-
- let responseText = null;
- let result;
- try {
- responseText = await response.text();
- result = JSON.parse(responseText);
- } catch (e) {
- const error = new Error('Invalid Anki response');
- error.data = {action, params, status: response.status, responseText};
- throw error;
- }
-
- if (isObject(result)) {
- const apiError = result.error;
- if (typeof apiError !== 'undefined') {
- const error = new Error(`Anki error: ${apiError}`);
- error.data = {action, params, status: response.status, apiError};
- throw error;
- }
- }
-
- return result;
- }
-
- _escapeQuery(text) {
- return text.replace(/"/g, '');
- }
-
- _fieldsToQuery(fields) {
- const fieldNames = Object.keys(fields);
- if (fieldNames.length === 0) {
- return '';
- }
-
- const key = fieldNames[0];
- return `"${key.toLowerCase()}:${this._escapeQuery(fields[key])}"`;
- }
-
- _getDuplicateScopeFromNote(note) {
- const {options} = note;
- if (typeof options === 'object' && options !== null) {
- const {duplicateScope} = options;
- if (typeof duplicateScope !== 'undefined') {
- return duplicateScope;
- }
- }
- return null;
- }
-}
diff --git a/ext/bg/js/audio-downloader.js b/ext/bg/js/audio-downloader.js
deleted file mode 100644
index 4e77419b..00000000
--- a/ext/bg/js/audio-downloader.js
+++ /dev/null
@@ -1,317 +0,0 @@
-/*
- * Copyright (C) 2017-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * JsonSchemaValidator
- * NativeSimpleDOMParser
- * SimpleDOMParser
- */
-
-class AudioDownloader {
- constructor({japaneseUtil, requestBuilder}) {
- this._japaneseUtil = japaneseUtil;
- this._requestBuilder = requestBuilder;
- this._customAudioListSchema = null;
- this._schemaValidator = null;
- this._getInfoHandlers = new Map([
- ['jpod101', this._getInfoJpod101.bind(this)],
- ['jpod101-alternate', this._getInfoJpod101Alternate.bind(this)],
- ['jisho', this._getInfoJisho.bind(this)],
- ['text-to-speech', this._getInfoTextToSpeech.bind(this)],
- ['text-to-speech-reading', this._getInfoTextToSpeechReading.bind(this)],
- ['custom', this._getInfoCustom.bind(this)]
- ]);
- }
-
- async getExpressionAudioInfoList(source, expression, reading, details) {
- const handler = this._getInfoHandlers.get(source);
- if (typeof handler === 'function') {
- try {
- return await handler(expression, reading, details);
- } catch (e) {
- // NOP
- }
- }
- return [];
- }
-
- async downloadExpressionAudio(sources, expression, reading, details) {
- for (const source of sources) {
- const infoList = await this.getExpressionAudioInfoList(source, expression, reading, details);
- for (const info of infoList) {
- switch (info.type) {
- case 'url':
- try {
- return await this._downloadAudioFromUrl(info.url, source);
- } catch (e) {
- // NOP
- }
- break;
- }
- }
- }
-
- throw new Error('Could not download audio');
- }
-
- // Private
-
- _normalizeUrl(url, base) {
- return new URL(url, base).href;
- }
-
- async _getInfoJpod101(expression, reading) {
- let kana = reading;
- let kanji = expression;
-
- if (!kana && this._japaneseUtil.isStringEntirelyKana(kanji)) {
- kana = kanji;
- kanji = null;
- }
-
- const params = [];
- if (kanji) {
- params.push(`kanji=${encodeURIComponent(kanji)}`);
- }
- if (kana) {
- params.push(`kana=${encodeURIComponent(kana)}`);
- }
-
- const url = `https://assets.languagepod101.com/dictionary/japanese/audiomp3.php?${params.join('&')}`;
- return [{type: 'url', url}];
- }
-
- async _getInfoJpod101Alternate(expression, reading) {
- const fetchUrl = 'https://www.japanesepod101.com/learningcenter/reference/dictionary_post';
- const data = `post=dictionary_reference&match_type=exact&search_query=${encodeURIComponent(expression)}&vulgar=true`;
- const response = await this._requestBuilder.fetchAnonymous(fetchUrl, {
- method: 'POST',
- mode: 'cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded'
- },
- body: data
- });
- const responseText = await response.text();
-
- const dom = this._createSimpleDOMParser(responseText);
- for (const row of dom.getElementsByClassName('dc-result-row')) {
- try {
- const audio = dom.getElementByTagName('audio', row);
- if (audio === null) { continue; }
-
- const source = dom.getElementByTagName('source', audio);
- if (source === null) { continue; }
-
- let url = dom.getAttribute(source, 'src');
- if (url === null) { continue; }
-
- const htmlReadings = dom.getElementsByClassName('dc-vocab_kana');
- if (htmlReadings.length === 0) { continue; }
-
- const htmlReading = dom.getTextContent(htmlReadings[0]);
- if (htmlReading && (!reading || reading === htmlReading)) {
- url = this._normalizeUrl(url, response.url);
- return [{type: 'url', url}];
- }
- } catch (e) {
- // NOP
- }
- }
-
- throw new Error('Failed to find audio URL');
- }
-
- async _getInfoJisho(expression, reading) {
- const fetchUrl = `https://jisho.org/search/${expression}`;
- const response = await this._requestBuilder.fetchAnonymous(fetchUrl, {
- method: 'GET',
- mode: 'cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer'
- });
- const responseText = await response.text();
-
- const dom = this._createSimpleDOMParser(responseText);
- try {
- const audio = dom.getElementById(`audio_${expression}:${reading}`);
- if (audio !== null) {
- const source = dom.getElementByTagName('source', audio);
- if (source !== null) {
- let url = dom.getAttribute(source, 'src');
- if (url !== null) {
- url = this._normalizeUrl(url, response.url);
- return [{type: 'url', url}];
- }
- }
- }
- } catch (e) {
- // NOP
- }
-
- throw new Error('Failed to find audio URL');
- }
-
- async _getInfoTextToSpeech(expression, reading, {textToSpeechVoice}) {
- if (!textToSpeechVoice) {
- throw new Error('No voice');
- }
- return [{type: 'tts', text: expression, voice: textToSpeechVoice}];
- }
-
- async _getInfoTextToSpeechReading(expression, reading, {textToSpeechVoice}) {
- if (!textToSpeechVoice) {
- throw new Error('No voice');
- }
- return [{type: 'tts', text: reading || expression, voice: textToSpeechVoice}];
- }
-
- async _getInfoCustom(expression, reading, {customSourceUrl, customSourceType}) {
- if (typeof customSourceUrl !== 'string') {
- throw new Error('No custom URL defined');
- }
- const data = {expression, reading};
- const url = customSourceUrl.replace(/\{([^}]*)\}/g, (m0, m1) => (Object.prototype.hasOwnProperty.call(data, m1) ? `${data[m1]}` : m0));
-
- switch (customSourceType) {
- case 'json':
- return await this._getInfoCustomJson(url);
- default:
- return [{type: 'url', url}];
- }
- }
-
- async _getInfoCustomJson(url) {
- const response = await this._requestBuilder.fetchAnonymous(url, {
- method: 'GET',
- mode: 'cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer'
- });
-
- if (!response.ok) {
- throw new Error(`Invalid response: ${response.status}`);
- }
-
- const responseJson = await response.json();
-
- const schema = await this._getCustomAudioListSchema();
- if (this._schemaValidator === null) {
- this._schemaValidator = new JsonSchemaValidator();
- }
- this._schemaValidator.validate(responseJson, schema);
-
- const results = [];
- for (const {url: url2, name} of responseJson.audioSources) {
- const info = {type: 'url', url: url2};
- if (typeof name === 'string') { info.name = name; }
- results.push(info);
- }
- return results;
- }
-
- async _downloadAudioFromUrl(url, source) {
- const response = await this._requestBuilder.fetchAnonymous(url, {
- method: 'GET',
- mode: 'cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer'
- });
-
- if (!response.ok) {
- throw new Error(`Invalid response: ${response.status}`);
- }
-
- const arrayBuffer = await response.arrayBuffer();
-
- if (!await this._isAudioBinaryValid(arrayBuffer, source)) {
- throw new Error('Could not retrieve audio');
- }
-
- const data = this._arrayBufferToBase64(arrayBuffer);
- const contentType = response.headers.get('Content-Type');
- return {data, contentType};
- }
-
- async _isAudioBinaryValid(arrayBuffer, source) {
- switch (source) {
- case 'jpod101':
- {
- const digest = await this._arrayBufferDigest(arrayBuffer);
- switch (digest) {
- case 'ae6398b5a27bc8c0a771df6c907ade794be15518174773c58c7c7ddd17098906': // Invalid audio
- return false;
- default:
- return true;
- }
- }
- default:
- return true;
- }
- }
-
- async _arrayBufferDigest(arrayBuffer) {
- const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array(arrayBuffer)));
- let digest = '';
- for (const byte of hash) {
- digest += byte.toString(16).padStart(2, '0');
- }
- return digest;
- }
-
- _arrayBufferToBase64(arrayBuffer) {
- return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
- }
-
- _createSimpleDOMParser(content) {
- if (typeof NativeSimpleDOMParser !== 'undefined' && NativeSimpleDOMParser.isSupported()) {
- return new NativeSimpleDOMParser(content);
- } else if (typeof SimpleDOMParser !== 'undefined' && SimpleDOMParser.isSupported()) {
- return new SimpleDOMParser(content);
- } else {
- throw new Error('DOM parsing not supported');
- }
- }
-
- async _getCustomAudioListSchema() {
- let schema = this._customAudioListSchema;
- if (schema === null) {
- const url = chrome.runtime.getURL('/data/schemas/custom-audio-list-schema.json');
- const response = await fetch(url, {
- method: 'GET',
- mode: 'no-cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer'
- });
- schema = await response.json();
- this._customAudioListSchema = schema;
- }
- return schema;
- }
-}
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
deleted file mode 100644
index 3bb23310..00000000
--- a/ext/bg/js/backend.js
+++ /dev/null
@@ -1,2053 +0,0 @@
-/*
- * Copyright (C) 2016-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * AnkiConnect
- * AudioDownloader
- * ClipboardMonitor
- * ClipboardReader
- * DictionaryDatabase
- * Environment
- * JapaneseUtil
- * JsonSchemaValidator
- * Mecab
- * MediaUtility
- * ObjectPropertyAccessor
- * OptionsUtil
- * PermissionsUtil
- * ProfileConditions
- * RequestBuilder
- * Translator
- * wanakana
- */
-
-class Backend {
- constructor() {
- this._japaneseUtil = new JapaneseUtil(wanakana);
- this._environment = new Environment();
- this._dictionaryDatabase = new DictionaryDatabase();
- this._translator = new Translator({
- japaneseUtil: this._japaneseUtil,
- database: this._dictionaryDatabase
- });
- this._anki = new AnkiConnect();
- this._mecab = new Mecab();
- this._mediaUtility = new MediaUtility();
- this._clipboardReader = new ClipboardReader({
- // eslint-disable-next-line no-undef
- document: (typeof document === 'object' && document !== null ? document : null),
- pasteTargetSelector: '#clipboard-paste-target',
- imagePasteTargetSelector: '#clipboard-image-paste-target',
- mediaUtility: this._mediaUtility
- });
- this._clipboardMonitor = new ClipboardMonitor({
- japaneseUtil: this._japaneseUtil,
- clipboardReader: this._clipboardReader
- });
- this._options = null;
- this._profileConditionsSchemaValidator = new JsonSchemaValidator();
- this._profileConditionsSchemaCache = [];
- this._profileConditionsUtil = new ProfileConditions();
- this._defaultAnkiFieldTemplates = null;
- this._requestBuilder = new RequestBuilder();
- this._audioDownloader = new AudioDownloader({
- japaneseUtil: this._japaneseUtil,
- requestBuilder: this._requestBuilder
- });
- this._optionsUtil = new OptionsUtil();
-
- this._searchPopupTabId = null;
- this._searchPopupTabCreatePromise = null;
-
- this._isPrepared = false;
- this._prepareError = false;
- this._preparePromise = null;
- const {promise, resolve, reject} = deferPromise();
- this._prepareCompletePromise = promise;
- this._prepareCompleteResolve = resolve;
- this._prepareCompleteReject = reject;
-
- this._defaultBrowserActionTitle = null;
- this._badgePrepareDelayTimer = null;
- this._logErrorLevel = null;
- this._permissions = null;
- this._permissionsUtil = new PermissionsUtil();
-
- this._messageHandlers = new Map([
- ['requestBackendReadySignal', {async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this)}],
- ['optionsGet', {async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this)}],
- ['optionsGetFull', {async: false, contentScript: true, handler: this._onApiOptionsGetFull.bind(this)}],
- ['kanjiFind', {async: true, contentScript: true, handler: this._onApiKanjiFind.bind(this)}],
- ['termsFind', {async: true, contentScript: true, handler: this._onApiTermsFind.bind(this)}],
- ['textParse', {async: true, contentScript: true, handler: this._onApiTextParse.bind(this)}],
- ['getAnkiConnectVersion', {async: true, contentScript: true, handler: this._onApGetAnkiConnectVersion.bind(this)}],
- ['isAnkiConnected', {async: true, contentScript: true, handler: this._onApiIsAnkiConnected.bind(this)}],
- ['addAnkiNote', {async: true, contentScript: true, handler: this._onApiAddAnkiNote.bind(this)}],
- ['getAnkiNoteInfo', {async: true, contentScript: true, handler: this._onApiGetAnkiNoteInfo.bind(this)}],
- ['injectAnkiNoteMedia', {async: true, contentScript: true, handler: this._onApiInjectAnkiNoteMedia.bind(this)}],
- ['noteView', {async: true, contentScript: true, handler: this._onApiNoteView.bind(this)}],
- ['suspendAnkiCardsForNote', {async: true, contentScript: true, handler: this._onApiSuspendAnkiCardsForNote.bind(this)}],
- ['commandExec', {async: false, contentScript: true, handler: this._onApiCommandExec.bind(this)}],
- ['getExpressionAudioInfoList', {async: true, contentScript: true, handler: this._onApiGetExpressionAudioInfoList.bind(this)}],
- ['sendMessageToFrame', {async: false, contentScript: true, handler: this._onApiSendMessageToFrame.bind(this)}],
- ['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}],
- ['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}],
- ['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}],
- ['getStylesheetContent', {async: true, contentScript: true, handler: this._onApiGetStylesheetContent.bind(this)}],
- ['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}],
- ['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}],
- ['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}],
- ['getZoom', {async: true, contentScript: true, handler: this._onApiGetZoom.bind(this)}],
- ['getDefaultAnkiFieldTemplates', {async: false, contentScript: true, handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this)}],
- ['getDictionaryInfo', {async: true, contentScript: false, handler: this._onApiGetDictionaryInfo.bind(this)}],
- ['getDictionaryCounts', {async: true, contentScript: false, handler: this._onApiGetDictionaryCounts.bind(this)}],
- ['purgeDatabase', {async: true, contentScript: false, handler: this._onApiPurgeDatabase.bind(this)}],
- ['getMedia', {async: true, contentScript: true, handler: this._onApiGetMedia.bind(this)}],
- ['log', {async: false, contentScript: true, handler: this._onApiLog.bind(this)}],
- ['logIndicatorClear', {async: false, contentScript: true, handler: this._onApiLogIndicatorClear.bind(this)}],
- ['createActionPort', {async: false, contentScript: true, handler: this._onApiCreateActionPort.bind(this)}],
- ['modifySettings', {async: true, contentScript: true, handler: this._onApiModifySettings.bind(this)}],
- ['getSettings', {async: false, contentScript: true, handler: this._onApiGetSettings.bind(this)}],
- ['setAllSettings', {async: true, contentScript: false, handler: this._onApiSetAllSettings.bind(this)}],
- ['getOrCreateSearchPopup', {async: true, contentScript: true, handler: this._onApiGetOrCreateSearchPopup.bind(this)}],
- ['isTabSearchPopup', {async: true, contentScript: true, handler: this._onApiIsTabSearchPopup.bind(this)}],
- ['triggerDatabaseUpdated', {async: false, contentScript: true, handler: this._onApiTriggerDatabaseUpdated.bind(this)}],
- ['testMecab', {async: true, contentScript: true, handler: this._onApiTestMecab.bind(this)}]
- ]);
- this._messageHandlersWithProgress = new Map([
- ]);
-
- this._commandHandlers = new Map([
- ['toggleTextScanning', this._onCommandToggleTextScanning.bind(this)],
- ['openInfoPage', this._onCommandOpenInfoPage.bind(this)],
- ['openSettingsPage', this._onCommandOpenSettingsPage.bind(this)],
- ['openSearchPage', this._onCommandOpenSearchPage.bind(this)],
- ['openPopupWindow', this._onCommandOpenPopupWindow.bind(this)]
- ]);
- }
-
- prepare() {
- if (this._preparePromise === null) {
- const promise = this._prepareInternal();
- promise.then(
- (value) => {
- this._isPrepared = true;
- this._prepareCompleteResolve(value);
- },
- (error) => {
- this._prepareError = true;
- this._prepareCompleteReject(error);
- }
- );
- promise.finally(() => this._updateBadge());
- this._preparePromise = promise;
- }
- return this._prepareCompletePromise;
- }
-
- // Private
-
- _prepareInternalSync() {
- if (isObject(chrome.commands) && isObject(chrome.commands.onCommand)) {
- const onCommand = this._onWebExtensionEventWrapper(this._onCommand.bind(this));
- chrome.commands.onCommand.addListener(onCommand);
- }
-
- if (isObject(chrome.tabs) && isObject(chrome.tabs.onZoomChange)) {
- const onZoomChange = this._onWebExtensionEventWrapper(this._onZoomChange.bind(this));
- chrome.tabs.onZoomChange.addListener(onZoomChange);
- }
-
- const onConnect = this._onWebExtensionEventWrapper(this._onConnect.bind(this));
- chrome.runtime.onConnect.addListener(onConnect);
-
- const onMessage = this._onMessageWrapper.bind(this);
- chrome.runtime.onMessage.addListener(onMessage);
-
- const onPermissionsChanged = this._onWebExtensionEventWrapper(this._onPermissionsChanged.bind(this));
- chrome.permissions.onAdded.addListener(onPermissionsChanged);
- chrome.permissions.onRemoved.addListener(onPermissionsChanged);
- }
-
- async _prepareInternal() {
- try {
- this._prepareInternalSync();
-
- this._permissions = await this._permissionsUtil.getAllPermissions();
- this._defaultBrowserActionTitle = await this._getBrowserIconTitle();
- this._badgePrepareDelayTimer = setTimeout(() => {
- this._badgePrepareDelayTimer = null;
- this._updateBadge();
- }, 1000);
- this._updateBadge();
-
- yomichan.on('log', this._onLog.bind(this));
-
- await this._requestBuilder.prepare();
- await this._environment.prepare();
- this._clipboardReader.browser = this._environment.getInfo().browser;
-
- try {
- await this._dictionaryDatabase.prepare();
- } catch (e) {
- yomichan.logError(e);
- }
-
- const deinflectionReasions = await this._fetchAsset('/data/deinflect.json', true);
- this._translator.prepare(deinflectionReasions);
-
- await this._optionsUtil.prepare();
- this._defaultAnkiFieldTemplates = (await this._fetchAsset('/data/templates/default-anki-field-templates.handlebars')).trim();
- this._options = await this._optionsUtil.load();
-
- this._applyOptions('background');
-
- const options = this._getProfileOptions({current: true});
- if (options.general.showGuide) {
- this._openWelcomeGuidePage();
- }
-
- this._clipboardMonitor.on('change', this._onClipboardTextChange.bind(this));
-
- this._sendMessageAllTabsIgnoreResponse('backendReady', {});
- this._sendMessageIgnoreResponse({action: 'backendReady', params: {}});
- } catch (e) {
- yomichan.logError(e);
- throw e;
- } finally {
- if (this._badgePrepareDelayTimer !== null) {
- clearTimeout(this._badgePrepareDelayTimer);
- this._badgePrepareDelayTimer = null;
- }
- }
- }
-
- // Event handlers
-
- async _onClipboardTextChange({text}) {
- const {clipboard: {maximumSearchLength}} = this._getProfileOptions({current: true});
- if (text.length > maximumSearchLength) {
- text = text.substring(0, maximumSearchLength);
- }
- try {
- const {tab, created} = await this._getOrCreateSearchPopup();
- await this._focusTab(tab);
- await this._updateSearchQuery(tab.id, text, !created);
- } catch (e) {
- // NOP
- }
- }
-
- _onLog({level}) {
- const levelValue = this._getErrorLevelValue(level);
- if (levelValue <= this._getErrorLevelValue(this._logErrorLevel)) { return; }
-
- this._logErrorLevel = level;
- this._updateBadge();
- }
-
- // WebExtension event handlers (with prepared checks)
-
- _onWebExtensionEventWrapper(handler) {
- return (...args) => {
- if (this._isPrepared) {
- handler(...args);
- return;
- }
-
- this._prepareCompletePromise.then(
- () => { handler(...args); },
- () => {} // NOP
- );
- };
- }
-
- _onMessageWrapper(message, sender, sendResponse) {
- if (this._isPrepared) {
- return this._onMessage(message, sender, sendResponse);
- }
-
- this._prepareCompletePromise.then(
- () => { this._onMessage(message, sender, sendResponse); },
- () => { sendResponse(); }
- );
- return true;
- }
-
- // WebExtension event handlers
-
- _onCommand(command) {
- this._runCommand(command);
- }
-
- _onMessage({action, params}, sender, callback) {
- const messageHandler = this._messageHandlers.get(action);
- if (typeof messageHandler === 'undefined') { return false; }
-
- if (!messageHandler.contentScript) {
- try {
- this._validatePrivilegedMessageSender(sender);
- } catch (error) {
- callback({error: serializeError(error)});
- return false;
- }
- }
-
- return yomichan.invokeMessageHandler(messageHandler, params, callback, sender);
- }
-
- _onConnect(port) {
- try {
- let details;
- try {
- details = JSON.parse(port.name);
- } catch (e) {
- return;
- }
- if (details.name !== 'background-cross-frame-communication-port') { return; }
-
- const senderTabId = (port.sender && port.sender.tab ? port.sender.tab.id : null);
- if (typeof senderTabId !== 'number') {
- throw new Error('Port does not have an associated tab ID');
- }
- const senderFrameId = port.sender.frameId;
- if (typeof senderFrameId !== 'number') {
- throw new Error('Port does not have an associated frame ID');
- }
- let {targetTabId, targetFrameId} = details;
- if (typeof targetTabId !== 'number') {
- targetTabId = senderTabId;
- }
-
- const details2 = {
- name: 'cross-frame-communication-port',
- sourceTabId: senderTabId,
- sourceFrameId: senderFrameId
- };
- let forwardPort = chrome.tabs.connect(targetTabId, {frameId: targetFrameId, name: JSON.stringify(details2)});
-
- const cleanup = () => {
- this._checkLastError(chrome.runtime.lastError);
- if (forwardPort !== null) {
- forwardPort.disconnect();
- forwardPort = null;
- }
- if (port !== null) {
- port.disconnect();
- port = null;
- }
- };
-
- port.onMessage.addListener((message) => { forwardPort.postMessage(message); });
- forwardPort.onMessage.addListener((message) => { port.postMessage(message); });
- port.onDisconnect.addListener(cleanup);
- forwardPort.onDisconnect.addListener(cleanup);
- } catch (e) {
- port.disconnect();
- yomichan.logError(e);
- }
- }
-
- _onZoomChange({tabId, oldZoomFactor, newZoomFactor}) {
- this._sendMessageTabIgnoreResponse(tabId, {action: 'zoomChanged', params: {oldZoomFactor, newZoomFactor}});
- }
-
- _onPermissionsChanged() {
- this._checkPermissions();
- }
-
- // Message handlers
-
- _onApiRequestBackendReadySignal(_params, sender) {
- // tab ID isn't set in background (e.g. browser_action)
- const data = {action: 'backendReady', params: {}};
- if (typeof sender.tab === 'undefined') {
- this._sendMessageIgnoreResponse(data);
- return false;
- } else {
- this._sendMessageTabIgnoreResponse(sender.tab.id, data);
- return true;
- }
- }
-
- _onApiOptionsGet({optionsContext}) {
- return this._getProfileOptions(optionsContext);
- }
-
- _onApiOptionsGetFull() {
- return this._getOptionsFull();
- }
-
- async _onApiKanjiFind({text, optionsContext}) {
- const options = this._getProfileOptions(optionsContext);
- const {general: {maxResults}} = options;
- const findKanjiOptions = this._getTranslatorFindKanjiOptions(options);
- const definitions = await this._translator.findKanji(text, findKanjiOptions);
- definitions.splice(maxResults);
- return definitions;
- }
-
- async _onApiTermsFind({text, details, optionsContext}) {
- const options = this._getProfileOptions(optionsContext);
- const {general: {resultOutputMode: mode, maxResults}} = options;
- const findTermsOptions = this._getTranslatorFindTermsOptions(details, options);
- const [definitions, length] = await this._translator.findTerms(mode, text, findTermsOptions);
- definitions.splice(maxResults);
- return {length, definitions};
- }
-
- async _onApiTextParse({text, optionsContext}) {
- const options = this._getProfileOptions(optionsContext);
- const results = [];
-
- if (options.parsing.enableScanningParser) {
- results.push({
- source: 'scanning-parser',
- id: 'scan',
- content: await this._textParseScanning(text, options)
- });
- }
-
- if (options.parsing.enableMecabParser) {
- const mecabResults = await this._textParseMecab(text, options);
- for (const [mecabDictName, mecabDictResults] of mecabResults) {
- results.push({
- source: 'mecab',
- dictionary: mecabDictName,
- id: `mecab-${mecabDictName}`,
- content: mecabDictResults
- });
- }
- }
-
- return results;
- }
-
- async _onApGetAnkiConnectVersion() {
- return await this._anki.getVersion();
- }
-
- async _onApiIsAnkiConnected() {
- return await this._anki.isConnected();
- }
-
- async _onApiAddAnkiNote({note}) {
- return await this._anki.addNote(note);
- }
-
- async _onApiGetAnkiNoteInfo({notes}) {
- const results = [];
- const cannotAdd = [];
- const canAddArray = await this._anki.canAddNotes(notes);
-
- for (let i = 0; i < notes.length; ++i) {
- const note = notes[i];
- const canAdd = canAddArray[i];
- const info = {canAdd, noteIds: null};
- results.push(info);
- if (!canAdd) {
- cannotAdd.push({note, info});
- }
- }
-
- if (cannotAdd.length > 0) {
- const cannotAddNotes = cannotAdd.map(({note}) => note);
- const noteIdsArray = await this._anki.findNoteIds(cannotAddNotes);
- for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) {
- const noteIds = noteIdsArray[i];
- if (noteIds.length > 0) {
- cannotAdd[i].info.noteIds = noteIds;
- }
- }
- }
-
- return results;
- }
-
- async _onApiInjectAnkiNoteMedia({timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails}) {
- return await this._injectAnkNoteMedia(
- this._anki,
- timestamp,
- definitionDetails,
- audioDetails,
- screenshotDetails,
- clipboardDetails
- );
- }
-
- async _onApiNoteView({noteId}) {
- return await this._anki.guiBrowseNote(noteId);
- }
-
- async _onApiSuspendAnkiCardsForNote({noteId}) {
- const cardIds = await this._anki.findCardsForNote(noteId);
- const count = cardIds.length;
- if (count > 0) {
- const okay = await this._anki.suspendCards(cardIds);
- if (!okay) { return 0; }
- }
- return count;
- }
-
- _onApiCommandExec({command, params}) {
- return this._runCommand(command, params);
- }
-
- async _onApiGetExpressionAudioInfoList({source, expression, reading, details}) {
- return await this._audioDownloader.getExpressionAudioInfoList(source, expression, reading, details);
- }
-
- _onApiSendMessageToFrame({frameId: targetFrameId, action, params}, sender) {
- if (!(sender && sender.tab)) {
- return false;
- }
-
- const tabId = sender.tab.id;
- const frameId = sender.frameId;
- this._sendMessageTabIgnoreResponse(tabId, {action, params, frameId}, {frameId: targetFrameId});
- return true;
- }
-
- _onApiBroadcastTab({action, params}, sender) {
- if (!(sender && sender.tab)) {
- return false;
- }
-
- const tabId = sender.tab.id;
- const frameId = sender.frameId;
- this._sendMessageTabIgnoreResponse(tabId, {action, params, frameId});
- return true;
- }
-
- _onApiFrameInformationGet(params, sender) {
- const tab = sender.tab;
- const tabId = tab ? tab.id : void 0;
- const frameId = sender.frameId;
- return Promise.resolve({tabId, frameId});
- }
-
- _onApiInjectStylesheet({type, value}, sender) {
- return this._injectStylesheet(type, value, sender);
- }
-
- async _onApiGetStylesheetContent({url}) {
- if (!url.startsWith('/') || url.startsWith('//') || !url.endsWith('.css')) {
- throw new Error('Invalid URL');
- }
- return await this._fetchAsset(url);
- }
-
- _onApiGetEnvironmentInfo() {
- return this._environment.getInfo();
- }
-
- async _onApiClipboardGet() {
- return this._clipboardReader.getText();
- }
-
- async _onApiGetDisplayTemplatesHtml() {
- return await this._fetchAsset('/display-templates.html');
- }
-
- _onApiGetZoom(params, sender) {
- if (!sender || !sender.tab) {
- return Promise.reject(new Error('Invalid tab'));
- }
-
- return new Promise((resolve, reject) => {
- const tabId = sender.tab.id;
- if (!(
- chrome.tabs !== null &&
- typeof chrome.tabs === 'object' &&
- typeof chrome.tabs.getZoom === 'function'
- )) {
- // Not supported
- resolve({zoomFactor: 1.0});
- return;
- }
- chrome.tabs.getZoom(tabId, (zoomFactor) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve({zoomFactor});
- }
- });
- });
- }
-
- _onApiGetDefaultAnkiFieldTemplates() {
- return this._defaultAnkiFieldTemplates;
- }
-
- async _onApiGetDictionaryInfo() {
- return await this._dictionaryDatabase.getDictionaryInfo();
- }
-
- async _onApiGetDictionaryCounts({dictionaryNames, getTotal}) {
- return await this._dictionaryDatabase.getDictionaryCounts(dictionaryNames, getTotal);
- }
-
- async _onApiPurgeDatabase() {
- await this._dictionaryDatabase.purge();
- this._triggerDatabaseUpdated('dictionary', 'purge');
- }
-
- async _onApiGetMedia({targets}) {
- return await this._dictionaryDatabase.getMedia(targets);
- }
-
- _onApiLog({error, level, context}) {
- yomichan.log(deserializeError(error), level, context);
- }
-
- _onApiLogIndicatorClear() {
- if (this._logErrorLevel === null) { return; }
- this._logErrorLevel = null;
- this._updateBadge();
- }
-
- _onApiCreateActionPort(params, sender) {
- if (!sender || !sender.tab) { throw new Error('Invalid sender'); }
- const tabId = sender.tab.id;
- if (typeof tabId !== 'number') { throw new Error('Sender has invalid tab ID'); }
-
- const frameId = sender.frameId;
- const id = generateId(16);
- const details = {
- name: 'action-port',
- id
- };
-
- const port = chrome.tabs.connect(tabId, {name: JSON.stringify(details), frameId});
- try {
- this._createActionListenerPort(port, sender, this._messageHandlersWithProgress);
- } catch (e) {
- port.disconnect();
- throw e;
- }
-
- return details;
- }
-
- _onApiModifySettings({targets, source}) {
- return this._modifySettings(targets, source);
- }
-
- _onApiGetSettings({targets}) {
- const results = [];
- for (const target of targets) {
- try {
- const result = this._getSetting(target);
- results.push({result: clone(result)});
- } catch (e) {
- results.push({error: serializeError(e)});
- }
- }
- return results;
- }
-
- async _onApiSetAllSettings({value, source}) {
- this._optionsUtil.validate(value);
- this._options = clone(value);
- await this._saveOptions(source);
- }
-
- async _onApiGetOrCreateSearchPopup({focus=false, text=null}) {
- const {tab, created} = await this._getOrCreateSearchPopup();
- if (focus === true || (focus === 'ifCreated' && created)) {
- await this._focusTab(tab);
- }
- if (typeof text === 'string') {
- await this._updateSearchQuery(tab.id, text, !created);
- }
- return {tabId: tab.id, windowId: tab.windowId};
- }
-
- async _onApiIsTabSearchPopup({tabId}) {
- const baseUrl = chrome.runtime.getURL('/search.html');
- const tab = typeof tabId === 'number' ? await this._checkTabUrl(tabId, (url) => url.startsWith(baseUrl)) : null;
- return (tab !== null);
- }
-
- _onApiTriggerDatabaseUpdated({type, cause}) {
- this._triggerDatabaseUpdated(type, cause);
- }
-
- async _onApiTestMecab() {
- if (!this._mecab.isEnabled()) {
- throw new Error('MeCab not enabled');
- }
-
- let permissionsOkay = false;
- try {
- permissionsOkay = await this._permissionsUtil.hasPermissions({permissions: ['nativeMessaging']});
- } catch (e) {
- // NOP
- }
- if (!permissionsOkay) {
- throw new Error('Insufficient permissions');
- }
-
- const disconnect = !this._mecab.isConnected();
- try {
- const version = await this._mecab.getVersion();
- if (version === null) {
- throw new Error('Could not connect to native MeCab component');
- }
-
- const localVersion = this._mecab.getLocalVersion();
- if (version !== localVersion) {
- throw new Error(`MeCab component version not supported: ${version}`);
- }
- } finally {
- // Disconnect if the connection was previously disconnected
- if (disconnect && this._mecab.isEnabled() && this._mecab.isActive()) {
- this._mecab.disconnect();
- }
- }
-
- return true;
- }
-
- // Command handlers
-
- async _onCommandOpenSearchPage(params) {
- const {mode='existingOrNewTab', query} = params || {};
-
- const baseUrl = chrome.runtime.getURL('/search.html');
- const queryParams = {};
- if (query && query.length > 0) { queryParams.query = query; }
- const queryString = new URLSearchParams(queryParams).toString();
- let url = baseUrl;
- if (queryString.length > 0) {
- url += `?${queryString}`;
- }
-
- const predicate = ({url: url2}) => {
- if (url2 === null || !url2.startsWith(baseUrl)) { return false; }
- const parsedUrl = new URL(url2);
- const baseUrl2 = `${parsedUrl.origin}${parsedUrl.pathname}`;
- const mode2 = parsedUrl.searchParams.get('mode');
- return baseUrl2 === baseUrl && (mode2 === mode || (!mode2 && mode === 'existingOrNewTab'));
- };
-
- const openInTab = async () => {
- const tab = await this._findTabs(1000, false, predicate, false);
- if (tab !== null) {
- await this._focusTab(tab);
- if (queryParams.query) {
- await this._updateSearchQuery(tab.id, queryParams.query, true);
- }
- return true;
- }
- };
-
- switch (mode) {
- case 'existingOrNewTab':
- try {
- if (await openInTab()) { return; }
- } catch (e) {
- // NOP
- }
- await this._createTab(url);
- return;
- case 'newTab':
- await this._createTab(url);
- return;
- }
- }
-
- async _onCommandOpenInfoPage() {
- await this._openInfoPage();
- }
-
- async _onCommandOpenSettingsPage(params) {
- const {mode='existingOrNewTab'} = params || {};
- await this._openSettingsPage(mode);
- }
-
- async _onCommandToggleTextScanning() {
- const options = this._getProfileOptions({current: true});
- await this._modifySettings([{
- action: 'set',
- path: 'general.enable',
- value: !options.general.enable,
- scope: 'profile',
- optionsContext: {current: true}
- }], 'backend');
- }
-
- async _onCommandOpenPopupWindow() {
- await this._onApiGetOrCreateSearchPopup({focus: true});
- }
-
- // Utilities
-
- async _modifySettings(targets, source) {
- const results = [];
- for (const target of targets) {
- try {
- const result = this._modifySetting(target);
- results.push({result: clone(result)});
- } catch (e) {
- results.push({error: serializeError(e)});
- }
- }
- await this._saveOptions(source);
- return results;
- }
-
- _getOrCreateSearchPopup() {
- if (this._searchPopupTabCreatePromise === null) {
- const promise = this._getOrCreateSearchPopup2();
- this._searchPopupTabCreatePromise = promise;
- promise.then(() => { this._searchPopupTabCreatePromise = null; });
- }
- return this._searchPopupTabCreatePromise;
- }
-
- async _getOrCreateSearchPopup2() {
- // Use existing tab
- const baseUrl = chrome.runtime.getURL('/search.html');
- const urlPredicate = (url) => url !== null && url.startsWith(baseUrl);
- if (this._searchPopupTabId !== null) {
- const tab = await this._checkTabUrl(this._searchPopupTabId, urlPredicate);
- if (tab !== null) {
- return {tab, created: false};
- }
- this._searchPopupTabId = null;
- }
-
- // Find existing tab
- const existingTabInfo = await this._findSearchPopupTab(urlPredicate);
- if (existingTabInfo !== null) {
- const existingTab = existingTabInfo.tab;
- this._searchPopupTabId = existingTab.id;
- return {tab: existingTab, created: false};
- }
-
- // chrome.windows not supported (e.g. on Firefox mobile)
- if (!isObject(chrome.windows)) {
- throw new Error('Window creation not supported');
- }
-
- // Create a new window
- const options = this._getProfileOptions({current: true});
- const createData = this._getSearchPopupWindowCreateData(baseUrl, options);
- const {popupWindow: {windowState}} = options;
- const popupWindow = await this._createWindow(createData);
- if (windowState !== 'normal') {
- await this._updateWindow(popupWindow.id, {state: windowState});
- }
-
- const {tabs} = popupWindow;
- if (tabs.length === 0) {
- throw new Error('Created window did not contain a tab');
- }
-
- const tab = tabs[0];
- await this._waitUntilTabFrameIsReady(tab.id, 0, 2000);
-
- await this._sendMessageTabPromise(
- tab.id,
- {action: 'setMode', params: {mode: 'popup'}},
- {frameId: 0}
- );
-
- this._searchPopupTabId = tab.id;
- return {tab, created: true};
- }
-
- async _findSearchPopupTab(urlPredicate) {
- const predicate = async ({url, tab}) => {
- if (!urlPredicate(url)) { return false; }
- try {
- const mode = await this._sendMessageTabPromise(
- tab.id,
- {action: 'getMode', params: {}},
- {frameId: 0}
- );
- return mode === 'popup';
- } catch (e) {
- return false;
- }
- };
- return await this._findTabs(1000, false, predicate, true);
- }
-
- _getSearchPopupWindowCreateData(url, options) {
- const {popupWindow: {width, height, left, top, useLeft, useTop, windowType}} = options;
- return {
- url,
- width,
- height,
- left: useLeft ? left : void 0,
- top: useTop ? top : void 0,
- type: windowType,
- state: 'normal'
- };
- }
-
- _createWindow(createData) {
- return new Promise((resolve, reject) => {
- chrome.windows.create(
- createData,
- (result) => {
- const error = chrome.runtime.lastError;
- if (error) {
- reject(new Error(error.message));
- } else {
- resolve(result);
- }
- }
- );
- });
- }
-
- _updateWindow(windowId, updateInfo) {
- return new Promise((resolve, reject) => {
- chrome.windows.update(
- windowId,
- updateInfo,
- (result) => {
- const error = chrome.runtime.lastError;
- if (error) {
- reject(new Error(error.message));
- } else {
- resolve(result);
- }
- }
- );
- });
- }
-
- _updateSearchQuery(tabId, text, animate) {
- return this._sendMessageTabPromise(
- tabId,
- {action: 'updateSearchQuery', params: {text, animate}},
- {frameId: 0}
- );
- }
-
- _applyOptions(source) {
- const options = this._getProfileOptions({current: true});
- this._updateBadge();
-
- this._anki.server = options.anki.server;
- this._anki.enabled = options.anki.enable;
-
- this._mecab.setEnabled(options.parsing.enableMecabParser);
-
- if (options.clipboard.enableBackgroundMonitor) {
- this._clipboardMonitor.start();
- } else {
- this._clipboardMonitor.stop();
- }
-
- this._sendMessageAllTabsIgnoreResponse('optionsUpdated', {source});
- }
-
- _getOptionsFull(useSchema=false) {
- const options = this._options;
- return useSchema ? this._optionsUtil.createValidatingProxy(options) : options;
- }
-
- _getProfileOptions(optionsContext, useSchema=false) {
- return this._getProfile(optionsContext, useSchema).options;
- }
-
- _getProfile(optionsContext, useSchema=false) {
- const options = this._getOptionsFull(useSchema);
- const profiles = options.profiles;
- if (optionsContext.current) {
- return profiles[options.profileCurrent];
- }
- if (typeof optionsContext.index === 'number') {
- return profiles[optionsContext.index];
- }
- const profile = this._getProfileFromContext(options, optionsContext);
- return profile !== null ? profile : profiles[options.profileCurrent];
- }
-
- _getProfileFromContext(options, optionsContext) {
- optionsContext = this._profileConditionsUtil.normalizeContext(optionsContext);
-
- let index = 0;
- for (const profile of options.profiles) {
- const conditionGroups = profile.conditionGroups;
-
- let schema;
- if (index < this._profileConditionsSchemaCache.length) {
- schema = this._profileConditionsSchemaCache[index];
- } else {
- schema = this._profileConditionsUtil.createSchema(conditionGroups);
- this._profileConditionsSchemaCache.push(schema);
- }
-
- if (conditionGroups.length > 0 && this._profileConditionsSchemaValidator.isValid(optionsContext, schema)) {
- return profile;
- }
- ++index;
- }
-
- return null;
- }
-
- _clearProfileConditionsSchemaCache() {
- this._profileConditionsSchemaCache = [];
- this._profileConditionsSchemaValidator.clearCache();
- }
-
- _checkLastError() {
- // NOP
- }
-
- _runCommand(command, params) {
- const handler = this._commandHandlers.get(command);
- if (typeof handler !== 'function') { return false; }
-
- handler(params);
- return true;
- }
-
- async _textParseScanning(text, options) {
- const jp = this._japaneseUtil;
- const {scanning: {length: scanningLength}, parsing: {readingMode}} = options;
- const findTermsOptions = this._getTranslatorFindTermsOptions({wildcard: null}, options);
- const results = [];
- while (text.length > 0) {
- const term = [];
- const [definitions, sourceLength] = await this._translator.findTerms(
- 'simple',
- text.substring(0, scanningLength),
- findTermsOptions
- );
- if (definitions.length > 0 && sourceLength > 0) {
- const {expression, reading} = definitions[0];
- const source = text.substring(0, sourceLength);
- for (const {text: text2, furigana} of jp.distributeFuriganaInflected(expression, reading, source)) {
- const reading2 = jp.convertReading(text2, furigana, readingMode);
- term.push({text: text2, reading: reading2});
- }
- text = text.substring(source.length);
- } else {
- const reading = jp.convertReading(text[0], '', readingMode);
- term.push({text: text[0], reading});
- text = text.substring(1);
- }
- results.push(term);
- }
- return results;
- }
-
- async _textParseMecab(text, options) {
- const jp = this._japaneseUtil;
- const {parsing: {readingMode}} = options;
-
- let parseTextResults;
- try {
- parseTextResults = await this._mecab.parseText(text);
- } catch (e) {
- return [];
- }
-
- const results = [];
- for (const {name, lines} of parseTextResults) {
- const result = [];
- for (const line of lines) {
- for (const {expression, reading, source} of line) {
- const term = [];
- for (const {text: text2, furigana} of jp.distributeFuriganaInflected(
- expression.length > 0 ? expression : source,
- jp.convertKatakanaToHiragana(reading),
- source
- )) {
- const reading2 = jp.convertReading(text2, furigana, readingMode);
- term.push({text: text2, reading: reading2});
- }
- result.push(term);
- }
- result.push([{text: '\n', reading: ''}]);
- }
- results.push([name, result]);
- }
- return results;
- }
-
- _createActionListenerPort(port, sender, handlers) {
- let hasStarted = false;
- let messageString = '';
-
- const onProgress = (...data) => {
- try {
- if (port === null) { return; }
- port.postMessage({type: 'progress', data});
- } catch (e) {
- // NOP
- }
- };
-
- const onMessage = (message) => {
- if (hasStarted) { return; }
-
- try {
- const {action, data} = message;
- switch (action) {
- case 'fragment':
- messageString += data;
- break;
- case 'invoke':
- {
- hasStarted = true;
- port.onMessage.removeListener(onMessage);
-
- const messageData = JSON.parse(messageString);
- messageString = null;
- onMessageComplete(messageData);
- }
- break;
- }
- } catch (e) {
- cleanup(e);
- }
- };
-
- const onMessageComplete = async (message) => {
- try {
- const {action, params} = message;
- port.postMessage({type: 'ack'});
-
- const messageHandler = handlers.get(action);
- if (typeof messageHandler === 'undefined') {
- throw new Error('Invalid action');
- }
- const {handler, async, contentScript} = messageHandler;
-
- if (!contentScript) {
- this._validatePrivilegedMessageSender(sender);
- }
-
- const promiseOrResult = handler(params, sender, onProgress);
- const result = async ? await promiseOrResult : promiseOrResult;
- port.postMessage({type: 'complete', data: result});
- } catch (e) {
- cleanup(e);
- }
- };
-
- const onDisconnect = () => {
- cleanup(null);
- };
-
- const cleanup = (error) => {
- if (port === null) { return; }
- if (error !== null) {
- port.postMessage({type: 'error', data: serializeError(error)});
- }
- if (!hasStarted) {
- port.onMessage.removeListener(onMessage);
- }
- port.onDisconnect.removeListener(onDisconnect);
- port = null;
- handlers = null;
- };
-
- port.onMessage.addListener(onMessage);
- port.onDisconnect.addListener(onDisconnect);
- }
-
- _getErrorLevelValue(errorLevel) {
- switch (errorLevel) {
- case 'info': return 0;
- case 'debug': return 0;
- case 'warn': return 1;
- case 'error': return 2;
- default: return 0;
- }
- }
-
- _getModifySettingObject(target) {
- const scope = target.scope;
- switch (scope) {
- case 'profile':
- if (!isObject(target.optionsContext)) { throw new Error('Invalid optionsContext'); }
- return this._getProfileOptions(target.optionsContext, true);
- case 'global':
- return this._getOptionsFull(true);
- default:
- throw new Error(`Invalid scope: ${scope}`);
- }
- }
-
- _getSetting(target) {
- const options = this._getModifySettingObject(target);
- const accessor = new ObjectPropertyAccessor(options);
- const {path} = target;
- if (typeof path !== 'string') { throw new Error('Invalid path'); }
- return accessor.get(ObjectPropertyAccessor.getPathArray(path));
- }
-
- _modifySetting(target) {
- const options = this._getModifySettingObject(target);
- const accessor = new ObjectPropertyAccessor(options);
- const action = target.action;
- switch (action) {
- case 'set':
- {
- const {path, value} = target;
- if (typeof path !== 'string') { throw new Error('Invalid path'); }
- const pathArray = ObjectPropertyAccessor.getPathArray(path);
- accessor.set(pathArray, value);
- return accessor.get(pathArray);
- }
- case 'delete':
- {
- const {path} = target;
- if (typeof path !== 'string') { throw new Error('Invalid path'); }
- accessor.delete(ObjectPropertyAccessor.getPathArray(path));
- return true;
- }
- case 'swap':
- {
- const {path1, path2} = target;
- if (typeof path1 !== 'string') { throw new Error('Invalid path1'); }
- if (typeof path2 !== 'string') { throw new Error('Invalid path2'); }
- accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2));
- return true;
- }
- case 'splice':
- {
- const {path, start, deleteCount, items} = target;
- if (typeof path !== 'string') { throw new Error('Invalid path'); }
- if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); }
- if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); }
- if (!Array.isArray(items)) { throw new Error('Invalid items'); }
- const array = accessor.get(ObjectPropertyAccessor.getPathArray(path));
- if (!Array.isArray(array)) { throw new Error('Invalid target type'); }
- return array.splice(start, deleteCount, ...items);
- }
- default:
- throw new Error(`Unknown action: ${action}`);
- }
- }
-
- _validatePrivilegedMessageSender(sender) {
- const url = sender.url;
- if (!(typeof url === 'string' && yomichan.isExtensionUrl(url))) {
- throw new Error('Invalid message sender');
- }
- }
-
- _getBrowserIconTitle() {
- return (
- isObject(chrome.browserAction) &&
- typeof chrome.browserAction.getTitle === 'function' ?
- new Promise((resolve) => chrome.browserAction.getTitle({}, resolve)) :
- Promise.resolve('')
- );
- }
-
- _updateBadge() {
- let title = this._defaultBrowserActionTitle;
- if (title === null || !isObject(chrome.browserAction)) {
- // Not ready or invalid
- return;
- }
-
- let text = '';
- let color = null;
- let status = null;
-
- if (this._logErrorLevel !== null) {
- switch (this._logErrorLevel) {
- case 'error':
- text = '!!';
- color = '#f04e4e';
- status = 'Error';
- break;
- default: // 'warn'
- text = '!';
- color = '#f0ad4e';
- status = 'Warning';
- break;
- }
- } else if (!this._isPrepared) {
- if (this._prepareError) {
- text = '!!';
- color = '#f04e4e';
- status = 'Error';
- } else if (this._badgePrepareDelayTimer === null) {
- text = '...';
- color = '#f0ad4e';
- status = 'Loading';
- }
- } else {
- const options = this._getProfileOptions({current: true});
- if (!options.general.enable) {
- text = 'off';
- color = '#555555';
- status = 'Disabled';
- } else if (!this._hasRequiredPermissionsForSettings(options)) {
- text = '!';
- color = '#f0ad4e';
- status = 'Some settings require additional permissions';
- } else if (!this._isAnyDictionaryEnabled(options)) {
- text = '!';
- color = '#f0ad4e';
- status = 'No dictionaries installed';
- }
- }
-
- if (color !== null && typeof chrome.browserAction.setBadgeBackgroundColor === 'function') {
- chrome.browserAction.setBadgeBackgroundColor({color});
- }
- if (text !== null && typeof chrome.browserAction.setBadgeText === 'function') {
- chrome.browserAction.setBadgeText({text});
- }
- if (typeof chrome.browserAction.setTitle === 'function') {
- if (status !== null) {
- title = `${title} - ${status}`;
- }
- chrome.browserAction.setTitle({title});
- }
- }
-
- _isAnyDictionaryEnabled(options) {
- for (const {enabled} of Object.values(options.dictionaries)) {
- if (enabled) {
- return true;
- }
- }
- return false;
- }
-
- _anyOptionsMatches(predicate) {
- for (const {options} of this._options.profiles) {
- const value = predicate(options);
- if (value) { return value; }
- }
- return false;
- }
-
- async _getTabUrl(tabId) {
- try {
- const {url} = await this._sendMessageTabPromise(
- tabId,
- {action: 'getUrl', params: {}},
- {frameId: 0}
- );
- if (typeof url === 'string') {
- return url;
- }
- } catch (e) {
- // NOP
- }
- return null;
- }
-
- _getAllTabs() {
- return new Promise((resolve, reject) => {
- chrome.tabs.query({}, (tabs) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(tabs);
- }
- });
- });
- }
-
- async _findTabs(timeout, multiple, predicate, predicateIsAsync) {
- // This function works around the need to have the "tabs" permission to access tab.url.
- const tabs = await this._getAllTabs();
-
- let done = false;
- const checkTab = async (tab, add) => {
- const url = await this._getTabUrl(tab.id);
-
- if (done) { return; }
-
- let okay = false;
- const item = {tab, url};
- try {
- okay = predicate(item);
- if (predicateIsAsync) { okay = await okay; }
- } catch (e) {
- // NOP
- }
-
- if (okay && !done) {
- if (add(item)) {
- done = true;
- }
- }
- };
-
- if (multiple) {
- const results = [];
- const add = (value) => {
- results.push(value);
- return false;
- };
- const checkTabPromises = tabs.map((tab) => checkTab(tab, add));
- await Promise.race([
- Promise.all(checkTabPromises),
- promiseTimeout(timeout)
- ]);
- return results;
- } else {
- const {promise, resolve} = deferPromise();
- let result = null;
- const add = (value) => {
- result = value;
- resolve();
- return true;
- };
- const checkTabPromises = tabs.map((tab) => checkTab(tab, add));
- await Promise.race([
- promise,
- Promise.all(checkTabPromises),
- promiseTimeout(timeout)
- ]);
- resolve();
- return result;
- }
- }
-
- async _focusTab(tab) {
- await new Promise((resolve, reject) => {
- chrome.tabs.update(tab.id, {active: true}, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } 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, reject) => {
- chrome.windows.get(tab.windowId, {}, (value) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(value);
- }
- });
- });
- if (!tabWindow.focused) {
- await new Promise((resolve, reject) => {
- chrome.windows.update(tab.windowId, {focused: true}, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- });
- }
- } catch (e) {
- // Edge throws exception for no reason here.
- }
- }
-
- _waitUntilTabFrameIsReady(tabId, frameId, timeout=null) {
- return new Promise((resolve, reject) => {
- let timer = null;
- let onMessage = (message, sender) => {
- if (
- !sender.tab ||
- sender.tab.id !== tabId ||
- sender.frameId !== frameId ||
- !isObject(message) ||
- message.action !== 'yomichanReady'
- ) {
- return;
- }
-
- cleanup();
- resolve();
- };
- const cleanup = () => {
- if (timer !== null) {
- clearTimeout(timer);
- timer = null;
- }
- if (onMessage !== null) {
- chrome.runtime.onMessage.removeListener(onMessage);
- onMessage = null;
- }
- };
-
- chrome.runtime.onMessage.addListener(onMessage);
-
- this._sendMessageTabPromise(tabId, {action: 'isReady'}, {frameId})
- .then(
- (value) => {
- if (!value) { return; }
- cleanup();
- resolve();
- },
- () => {} // NOP
- );
-
- if (timeout !== null) {
- timer = setTimeout(() => {
- timer = null;
- cleanup();
- reject(new Error('Timeout'));
- }, timeout);
- }
- });
- }
-
- async _fetchAsset(url, json=false) {
- const response = await fetch(chrome.runtime.getURL(url), {
- method: 'GET',
- mode: 'no-cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer'
- });
- if (!response.ok) {
- throw new Error(`Failed to fetch ${url}: ${response.status}`);
- }
- return await (json ? response.json() : response.text());
- }
-
- _sendMessageIgnoreResponse(...args) {
- const callback = () => this._checkLastError(chrome.runtime.lastError);
- chrome.runtime.sendMessage(...args, callback);
- }
-
- _sendMessageTabIgnoreResponse(...args) {
- const callback = () => this._checkLastError(chrome.runtime.lastError);
- chrome.tabs.sendMessage(...args, callback);
- }
-
- _sendMessageAllTabsIgnoreResponse(action, params) {
- const callback = () => this._checkLastError(chrome.runtime.lastError);
- chrome.tabs.query({}, (tabs) => {
- for (const tab of tabs) {
- chrome.tabs.sendMessage(tab.id, {action, params}, callback);
- }
- });
- }
-
- _sendMessageTabPromise(...args) {
- return new Promise((resolve, reject) => {
- const callback = (response) => {
- try {
- resolve(yomichan.getMessageResponseResult(response));
- } catch (error) {
- reject(error);
- }
- };
-
- chrome.tabs.sendMessage(...args, callback);
- });
- }
-
- async _checkTabUrl(tabId, urlPredicate) {
- let tab;
- try {
- tab = await this._getTabById(tabId);
- } catch (e) {
- return null;
- }
-
- const url = await this._getTabUrl(tabId);
- const isValidTab = urlPredicate(url);
- return isValidTab ? tab : null;
- }
-
- async _getScreenshot(tabId, frameId, format, quality) {
- const tab = await this._getTabById(tabId);
- const {windowId} = tab;
-
- let token = null;
- try {
- if (typeof tabId === 'number' && typeof frameId === 'number') {
- const action = 'setAllVisibleOverride';
- const params = {value: false, priority: 0, awaitFrame: true};
- token = await this._sendMessageTabPromise(tabId, {action, params}, {frameId});
- }
-
- return await new Promise((resolve, reject) => {
- chrome.tabs.captureVisibleTab(windowId, {format, quality}, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- });
- });
- } finally {
- if (token !== null) {
- const action = 'clearAllVisibleOverride';
- const params = {token};
- try {
- await this._sendMessageTabPromise(tabId, {action, params}, {frameId});
- } catch (e) {
- // NOP
- }
- }
- }
- }
-
- async _downloadDefinitionAudio(sources, expression, reading, details) {
- return await this._audioDownloader.downloadExpressionAudio(sources, expression, reading, details);
- }
-
- async _injectAnkNoteMedia(ankiConnect, timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails) {
- let screenshotFileName = null;
- let clipboardImageFileName = null;
- let clipboardText = null;
- let audioFileName = null;
- const errors = [];
-
- try {
- if (screenshotDetails !== null) {
- screenshotFileName = await this._injectAnkNoteScreenshot(ankiConnect, timestamp, definitionDetails, screenshotDetails);
- }
- } catch (e) {
- errors.push(serializeError(e));
- }
-
- try {
- if (clipboardDetails !== null && clipboardDetails.image) {
- clipboardImageFileName = await this._injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails);
- }
- } catch (e) {
- errors.push(serializeError(e));
- }
-
- try {
- if (clipboardDetails !== null && clipboardDetails.text) {
- clipboardText = await this._clipboardReader.getText();
- }
- } catch (e) {
- errors.push(serializeError(e));
- }
-
- try {
- if (audioDetails !== null) {
- audioFileName = await this._injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, audioDetails);
- }
- } catch (e) {
- errors.push(serializeError(e));
- }
-
- return {
- result: {
- screenshotFileName,
- clipboardImageFileName,
- clipboardText,
- audioFileName
- },
- errors
- };
- }
-
- async _injectAnkNoteAudio(ankiConnect, timestamp, definitionDetails, details) {
- const {type, expression, reading} = definitionDetails;
- if (
- type === 'kanji' ||
- typeof expression !== 'string' ||
- typeof reading !== 'string' ||
- (expression.length === 0 && reading.length === 0)
- ) {
- return null;
- }
-
- const {sources, customSourceUrl, customSourceType} = details;
- let data;
- let contentType;
- try {
- ({data, contentType} = await this._downloadDefinitionAudio(
- sources,
- expression,
- reading,
- {
- textToSpeechVoice: null,
- customSourceUrl,
- customSourceType,
- binary: true,
- disableCache: true
- }
- ));
- } catch (e) {
- // No audio
- return null;
- }
-
- let extension = this._mediaUtility.getFileExtensionFromAudioMediaType(contentType);
- if (extension === null) { extension = '.mp3'; }
- let fileName = this._generateAnkiNoteMediaFileName('yomichan_audio', extension, timestamp, definitionDetails);
- fileName = fileName.replace(/\]/g, '');
- await ankiConnect.storeMediaFile(fileName, data);
-
- return fileName;
- }
-
- async _injectAnkNoteScreenshot(ankiConnect, timestamp, definitionDetails, details) {
- const {tabId, frameId, format, quality} = details;
- const dataUrl = await this._getScreenshot(tabId, frameId, format, quality);
-
- const {mediaType, data} = this._getDataUrlInfo(dataUrl);
- const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType);
- if (extension === null) {
- throw new Error('Unknown media type for screenshot image');
- }
-
- const fileName = this._generateAnkiNoteMediaFileName('yomichan_browser_screenshot', extension, timestamp, definitionDetails);
- await ankiConnect.storeMediaFile(fileName, data);
-
- return fileName;
- }
-
- async _injectAnkNoteClipboardImage(ankiConnect, timestamp, definitionDetails) {
- const dataUrl = await this._clipboardReader.getImage();
- if (dataUrl === null) {
- return null;
- }
-
- const {mediaType, data} = this._getDataUrlInfo(dataUrl);
- const extension = this._mediaUtility.getFileExtensionFromImageMediaType(mediaType);
- if (extension === null) {
- throw new Error('Unknown media type for clipboard image');
- }
-
- const fileName = this._generateAnkiNoteMediaFileName('yomichan_clipboard_image', extension, timestamp, definitionDetails);
- await ankiConnect.storeMediaFile(fileName, data);
-
- return fileName;
- }
-
- _generateAnkiNoteMediaFileName(prefix, extension, timestamp, definitionDetails) {
- let fileName = prefix;
-
- switch (definitionDetails.type) {
- case 'kanji':
- {
- const {character} = definitionDetails;
- if (character) { fileName += `_${character}`; }
- }
- break;
- default:
- {
- const {reading, expression} = definitionDetails;
- if (reading) { fileName += `_${reading}`; }
- if (expression) { fileName += `_${expression}`; }
- }
- break;
- }
-
- fileName += `_${this._ankNoteDateToString(new Date(timestamp))}`;
- fileName += extension;
-
- fileName = this._replaceInvalidFileNameCharacters(fileName);
-
- return fileName;
- }
-
- _replaceInvalidFileNameCharacters(fileName) {
- // eslint-disable-next-line no-control-regex
- return fileName.replace(/[<>:"/\\|?*\x00-\x1F]/g, '-');
- }
-
- _ankNoteDateToString(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}`;
- }
-
- _getDataUrlInfo(dataUrl) {
- const match = /^data:([^,]*?)(;base64)?,/.exec(dataUrl);
- if (match === null) {
- throw new Error('Invalid data URL');
- }
-
- let mediaType = match[1];
- if (mediaType.length === 0) { mediaType = 'text/plain'; }
-
- let data = dataUrl.substring(match[0].length);
- if (typeof match[2] === 'undefined') { data = btoa(data); }
-
- return {mediaType, data};
- }
-
- _triggerDatabaseUpdated(type, cause) {
- this._translator.clearDatabaseCaches();
- this._sendMessageAllTabsIgnoreResponse('databaseUpdated', {type, cause});
- }
-
- async _saveOptions(source) {
- this._clearProfileConditionsSchemaCache();
- const options = this._getOptionsFull();
- await this._optionsUtil.save(options);
- this._applyOptions(source);
- }
-
- _getTranslatorFindTermsOptions(details, options) {
- const {wildcard} = details;
- const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options);
- const {
- general: {mainDictionary},
- scanning: {alphanumeric},
- translation: {
- convertHalfWidthCharacters,
- convertNumericCharacters,
- convertAlphabeticCharacters,
- convertHiraganaToKatakana,
- convertKatakanaToHiragana,
- collapseEmphaticSequences,
- textReplacements: textReplacementsOptions
- }
- } = options;
- const textReplacements = this._getTranslatorTextReplacements(textReplacementsOptions);
- return {
- wildcard,
- mainDictionary,
- alphanumeric,
- convertHalfWidthCharacters,
- convertNumericCharacters,
- convertAlphabeticCharacters,
- convertHiraganaToKatakana,
- convertKatakanaToHiragana,
- collapseEmphaticSequences,
- textReplacements,
- enabledDictionaryMap
- };
- }
-
- _getTranslatorFindKanjiOptions(options) {
- const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options);
- return {enabledDictionaryMap};
- }
-
- _getTranslatorEnabledDictionaryMap(options) {
- const enabledDictionaryMap = new Map();
- for (const [title, {enabled, priority, allowSecondarySearches}] of Object.entries(options.dictionaries)) {
- if (!enabled) { continue; }
- enabledDictionaryMap.set(title, {priority, allowSecondarySearches});
- }
- return enabledDictionaryMap;
- }
-
- _getTranslatorTextReplacements(textReplacementsOptions) {
- const textReplacements = [];
- for (const group of textReplacementsOptions.groups) {
- const textReplacementsEntries = [];
- for (let {pattern, ignoreCase, replacement} of group) {
- try {
- pattern = new RegExp(pattern, ignoreCase ? 'gi' : 'g');
- } catch (e) {
- // Invalid pattern
- continue;
- }
- textReplacementsEntries.push({pattern, replacement});
- }
- if (textReplacementsEntries.length > 0) {
- textReplacements.push(textReplacementsEntries);
- }
- }
- if (textReplacements.length === 0 || textReplacementsOptions.searchOriginal) {
- textReplacements.unshift(null);
- }
- return textReplacements;
- }
-
- async _openWelcomeGuidePage() {
- await this._createTab(chrome.runtime.getURL('/welcome.html'));
- }
-
- async _openInfoPage() {
- await this._createTab(chrome.runtime.getURL('/info.html'));
- }
-
- async _openSettingsPage(mode) {
- const {useSettingsV2} = this._options.global;
- const manifest = chrome.runtime.getManifest();
- const url = chrome.runtime.getURL(useSettingsV2 ? manifest.options_ui.page : '/settings-old.html');
- switch (mode) {
- case 'existingOrNewTab':
- if (useSettingsV2) {
- const predicate = ({url: url2}) => (url2 !== null && url2.startsWith(url));
- const tab = await this._findTabs(1000, false, predicate, false);
- if (tab !== null) {
- await this._focusTab(tab);
- } else {
- await this._createTab(url);
- }
- } else {
- await new Promise((resolve, reject) => {
- chrome.runtime.openOptionsPage(() => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- });
- }
- break;
- case 'newTab':
- await this._createTab(url);
- break;
- }
- }
-
- _createTab(url) {
- return new Promise((resolve, reject) => {
- chrome.tabs.create({url}, (tab) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(tab);
- }
- });
- });
- }
-
- _injectStylesheet(type, value, target) {
- if (isObject(chrome.tabs) && typeof chrome.tabs.insertCSS === 'function') {
- return this._injectStylesheetMV2(type, value, target);
- } else if (isObject(chrome.scripting) && typeof chrome.scripting.insertCSS === 'function') {
- return this._injectStylesheetMV3(type, value, target);
- } else {
- return Promise.reject(new Error('insertCSS function not available'));
- }
- }
-
- _injectStylesheetMV2(type, value, target) {
- return new Promise((resolve, reject) => {
- if (!target.tab) {
- reject(new Error('Invalid tab'));
- return;
- }
-
- const tabId = target.tab.id;
- const frameId = target.frameId;
- const details = (
- type === 'file' ?
- {
- file: value,
- runAt: 'document_start',
- cssOrigin: 'author',
- allFrames: false,
- matchAboutBlank: true
- } :
- {
- code: value,
- runAt: 'document_start',
- cssOrigin: 'user',
- allFrames: false,
- matchAboutBlank: true
- }
- );
- if (typeof frameId === 'number') {
- details.frameId = frameId;
- }
-
- chrome.tabs.insertCSS(tabId, details, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- });
- }
-
- _injectStylesheetMV3(type, value, target) {
- return new Promise((resolve, reject) => {
- if (!target.tab) {
- reject(new Error('Invalid tab'));
- return;
- }
-
- const tabId = target.tab.id;
- const frameId = target.frameId;
- const details = (
- type === 'file' ?
- {origin: chrome.scripting.StyleOrigin.AUTHOR, files: [value]} :
- {origin: chrome.scripting.StyleOrigin.USER, css: value}
- );
- details.target = {
- tabId,
- allFrames: false
- };
- if (typeof frameId === 'number') {
- details.target.frameIds = [frameId];
- }
-
- chrome.scripting.insertCSS(details, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- });
- }
-
- _getTabById(tabId) {
- return new Promise((resolve, reject) => {
- chrome.tabs.get(
- tabId,
- (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- }
- );
- });
- }
-
- async _checkPermissions() {
- this._permissions = await this._permissionsUtil.getAllPermissions();
- this._updateBadge();
- }
-
- _hasRequiredPermissionsForSettings(options) {
- return this._permissions === null || this._permissionsUtil.hasRequiredPermissionsForOptions(this._permissions, options);
- }
-}
diff --git a/ext/bg/js/background-main.js b/ext/bg/js/background-main.js
deleted file mode 100644
index 01e57d0f..00000000
--- a/ext/bg/js/background-main.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * Backend
- */
-
-(() => {
- const backend = new Backend();
- backend.prepare();
-})();
diff --git a/ext/bg/js/clipboard-monitor.js b/ext/bg/js/clipboard-monitor.js
deleted file mode 100644
index 7379d7ad..00000000
--- a/ext/bg/js/clipboard-monitor.js
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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 ClipboardMonitor extends EventDispatcher {
- constructor({japaneseUtil, clipboardReader}) {
- super();
- this._japaneseUtil = japaneseUtil;
- this._clipboardReader = clipboardReader;
- this._timerId = null;
- this._timerToken = null;
- this._interval = 250;
- this._previousText = null;
- }
-
- start() {
- this.stop();
-
- // 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 call
- // will exit early if the reference has changed.
- let canChange = false;
- const token = {};
- const intervalCallback = async () => {
- this._timerId = null;
-
- let text = null;
- try {
- text = await this._clipboardReader.getText();
- } catch (e) {
- // NOP
- }
- if (this._timerToken !== token) { return; }
-
- if (
- typeof text === 'string' &&
- (text = text.trim()).length > 0 &&
- text !== this._previousText
- ) {
- this._previousText = text;
- if (canChange && this._japaneseUtil.isStringPartiallyJapanese(text)) {
- this.trigger('change', {text});
- }
- }
-
- canChange = true;
- this._timerId = setTimeout(intervalCallback, this._interval);
- };
-
- this._timerToken = token;
-
- intervalCallback();
- }
-
- stop() {
- this._timerToken = null;
- this._previousText = null;
- if (this._timerId !== null) {
- clearTimeout(this._timerId);
- this._timerId = null;
- }
- }
-
- setPreviousText(text) {
- this._previousText = text;
- }
-}
diff --git a/ext/bg/js/clipboard-reader.js b/ext/bg/js/clipboard-reader.js
deleted file mode 100644
index 275c2d60..00000000
--- a/ext/bg/js/clipboard-reader.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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 which can read text and images from the clipboard.
- */
-class ClipboardReader {
- /**
- * Creates a new instances of a clipboard reader.
- * @param document The Document object to be used, or null for no support.
- * @param pasteTargetSelector The selector for the paste target element.
- * @param imagePasteTargetSelector The selector for the image paste target element.
- */
- constructor({document=null, pasteTargetSelector=null, imagePasteTargetSelector=null, mediaUtility=null}) {
- this._document = document;
- this._browser = null;
- this._pasteTarget = null;
- this._pasteTargetSelector = pasteTargetSelector;
- this._imagePasteTarget = null;
- this._imagePasteTargetSelector = imagePasteTargetSelector;
- this._mediaUtility = mediaUtility;
- }
-
- /**
- * Gets the browser being used.
- */
- get browser() {
- return this._browser;
- }
-
- /**
- * Assigns the browser being used.
- */
- set browser(value) {
- this._browser = value;
- }
-
- /**
- * Gets the text in the clipboard.
- * @returns A string containing the clipboard text.
- * @throws Error if not supported.
- */
- async getText() {
- /*
- Notes:
- document.execCommand('paste') doesn't work on Firefox.
- See: 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.
- */
- if (this._isFirefox()) {
- try {
- return await navigator.clipboard.readText();
- } catch (e) {
- // Error is undefined, due to permissions
- throw new Error('Cannot read clipboard text; check extension permissions');
- }
- }
-
- const document = this._document;
- if (document === null) {
- throw new Error('Clipboard reading not supported in this context');
- }
-
- let target = this._pasteTarget;
- if (target === null) {
- target = document.querySelector(this._pasteTargetSelector);
- if (target === null) {
- throw new Error('Clipboard paste target does not exist');
- }
- this._pasteTarget = target;
- }
-
- target.value = '';
- target.focus();
- document.execCommand('paste');
- const result = target.value;
- target.value = '';
- return (typeof result === 'string' ? result : '');
- }
-
- /**
- * Gets the first image in the clipboard.
- * @returns A string containing a data URL of the image file, or null if no image was found.
- * @throws Error if not supported.
- */
- async getImage() {
- // See browser-specific notes in getText
- if (
- this._isFirefox() &&
- this._mediaUtility !== null &&
- typeof navigator.clipboard !== 'undefined' &&
- typeof navigator.clipboard.read === 'function'
- ) {
- // This function is behind the Firefox flag: dom.events.asyncClipboard.dataTransfer
- let files;
- try {
- ({files} = await navigator.clipboard.read());
- } catch (e) {
- return null;
- }
-
- for (const file of files) {
- if (this._mediaUtility.getFileExtensionFromImageMediaType(file.type) !== null) {
- return await this._readFileAsDataURL(file);
- }
- }
- return null;
- }
-
- const document = this._document;
- if (document === null) {
- throw new Error('Clipboard reading not supported in this context');
- }
-
- let target = this._imagePasteTarget;
- if (target === null) {
- target = document.querySelector(this._imagePasteTargetSelector);
- if (target === null) {
- throw new Error('Clipboard paste target does not exist');
- }
- this._imagePasteTarget = target;
- }
-
- target.focus();
- document.execCommand('paste');
- const image = target.querySelector('img[src^="data:"]');
- const result = (image !== null ? image.getAttribute('src') : null);
- for (const image2 of target.querySelectorAll('img')) {
- image2.removeAttribute('src');
- }
- target.textContent = '';
- return result;
- }
-
- // Private
-
- _isFirefox() {
- return (this._browser === 'firefox' || this._browser === 'firefox-mobile');
- }
-
- _readFileAsDataURL(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => resolve(reader.result);
- reader.onerror = () => reject(reader.error);
- reader.readAsDataURL(file);
- });
- }
-}
diff --git a/ext/bg/js/context-main.js b/ext/bg/js/context-main.js
deleted file mode 100644
index 5cc56745..00000000
--- a/ext/bg/js/context-main.js
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Copyright (C) 2017-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * HotkeyHelpController
- * PermissionsUtil
- * api
- */
-
-class DisplayController {
- constructor() {
- this._optionsFull = null;
- this._permissionsUtil = new PermissionsUtil();
- }
-
- async prepare() {
- const manifest = chrome.runtime.getManifest();
-
- this._showExtensionInfo(manifest);
- this._setupEnvironment();
- this._setupButtonEvents('.action-open-search', 'openSearchPage', chrome.runtime.getURL('/search.html'));
- this._setupButtonEvents('.action-open-info', 'openInfoPage', chrome.runtime.getURL('/info.html'));
-
- const optionsFull = await api.optionsGetFull();
- this._optionsFull = optionsFull;
-
- this._setupHotkeys();
-
- const optionsPageUrl = optionsFull.global.useSettingsV2 ? manifest.options_ui.page : '/settings-old.html';
- this._setupButtonEvents('.action-open-settings', 'openSettingsPage', chrome.runtime.getURL(optionsPageUrl));
- this._setupButtonEvents('.action-open-permissions', null, chrome.runtime.getURL('/permissions.html'));
-
- const {profiles, profileCurrent} = optionsFull;
- const primaryProfile = (profileCurrent >= 0 && profileCurrent < profiles.length) ? profiles[profileCurrent] : null;
- if (primaryProfile !== null) {
- this._setupOptions(primaryProfile);
- }
-
- document.querySelector('.action-select-profile').hidden = (profiles.length <= 1);
-
- this._updateProfileSelect(profiles, profileCurrent);
-
- setTimeout(() => {
- document.body.dataset.loaded = 'true';
- }, 10);
- }
-
- // Private
-
- _showExtensionInfo(manifest) {
- const node = document.getElementById('extension-info');
- if (node === null) { return; }
-
- node.textContent = `${manifest.name} v${manifest.version}`;
- }
-
- _setupButtonEvents(selector, command, url) {
- const nodes = document.querySelectorAll(selector);
- for (const node of nodes) {
- if (typeof command === 'string') {
- node.addEventListener('click', (e) => {
- if (e.button !== 0) { return; }
- api.commandExec(command, {mode: e.ctrlKey ? 'newTab' : 'existingOrNewTab'});
- e.preventDefault();
- }, false);
- node.addEventListener('auxclick', (e) => {
- if (e.button !== 1) { return; }
- api.commandExec(command, {mode: 'newTab'});
- e.preventDefault();
- }, false);
- }
-
- if (typeof url === 'string') {
- node.href = url;
- node.target = '_blank';
- node.rel = 'noopener';
- }
- }
- }
-
- async _setupEnvironment() {
- const urlSearchParams = new URLSearchParams(location.search);
- let mode = urlSearchParams.get('mode');
- switch (mode) {
- case 'full':
- case 'mini':
- break;
- default:
- {
- let tab;
- try {
- tab = await this._getCurrentTab();
- } catch (e) {
- // NOP
- }
- mode = (tab ? 'full' : 'mini');
- }
- break;
- }
-
- document.documentElement.dataset.mode = mode;
- }
-
- _getCurrentTab() {
- return new Promise((resolve, reject) => {
- chrome.tabs.getCurrent((result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- });
- });
- }
-
- _setupOptions({options}) {
- const extensionEnabled = options.general.enable;
- const onToggleChanged = () => api.commandExec('toggleTextScanning');
- for (const toggle of document.querySelectorAll('#enable-search,#enable-search2')) {
- toggle.checked = extensionEnabled;
- toggle.addEventListener('change', onToggleChanged, false);
- }
- this._updateDictionariesEnabledWarnings(options);
- this._updatePermissionsWarnings(options);
- }
-
- async _setupHotkeys() {
- const hotkeyHelpController = new HotkeyHelpController();
- await hotkeyHelpController.prepare();
-
- const {profiles, profileCurrent} = this._optionsFull;
- const primaryProfile = (profileCurrent >= 0 && profileCurrent < profiles.length) ? profiles[profileCurrent] : null;
- if (primaryProfile !== null) {
- hotkeyHelpController.setOptions(primaryProfile.options);
- }
-
- hotkeyHelpController.setupNode(document.documentElement);
- }
-
- _updateProfileSelect(profiles, profileCurrent) {
- const select = document.querySelector('#profile-select');
- const optionGroup = document.querySelector('#profile-select-option-group');
- const fragment = document.createDocumentFragment();
- for (let i = 0, ii = profiles.length; i < ii; ++i) {
- const {name} = profiles[i];
- const option = document.createElement('option');
- option.textContent = name;
- option.value = `${i}`;
- fragment.appendChild(option);
- }
- optionGroup.textContent = '';
- optionGroup.appendChild(fragment);
- select.value = `${profileCurrent}`;
-
- select.addEventListener('change', this._onProfileSelectChange.bind(this), false);
- }
-
- _onProfileSelectChange(e) {
- const value = parseInt(e.currentTarget.value, 10);
- if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= this._optionsFull.profiles.length) {
- this._setPrimaryProfileIndex(value);
- }
- }
-
- async _setPrimaryProfileIndex(value) {
- return await api.modifySettings(
- [{
- action: 'set',
- path: 'profileCurrent',
- value,
- scope: 'global'
- }]
- );
- }
-
- async _updateDictionariesEnabledWarnings(options) {
- const noDictionariesEnabledWarnings = document.querySelectorAll('.no-dictionaries-enabled-warning');
- const dictionaries = await api.getDictionaryInfo();
-
- let enabledCount = 0;
- for (const {title} of dictionaries) {
- if (
- Object.prototype.hasOwnProperty.call(options.dictionaries, title) &&
- options.dictionaries[title].enabled
- ) {
- ++enabledCount;
- }
- }
-
- const hasEnabledDictionary = (enabledCount > 0);
- for (const node of noDictionariesEnabledWarnings) {
- node.hidden = hasEnabledDictionary;
- }
- }
-
- async _updatePermissionsWarnings(options) {
- const permissions = await this._permissionsUtil.getAllPermissions();
- if (this._permissionsUtil.hasRequiredPermissionsForOptions(permissions, options)) { return; }
-
- const warnings = document.querySelectorAll('.action-open-permissions,.permissions-required-warning');
- for (const node of warnings) {
- console.log(node);
- node.hidden = false;
- }
- }
-}
-
-(async () => {
- api.forwardLogsToBackend();
- await yomichan.backendReady();
-
- api.logIndicatorClear();
-
- const displayController = new DisplayController();
- displayController.prepare();
-
- yomichan.ready();
-})();
diff --git a/ext/bg/js/database.js b/ext/bg/js/database.js
deleted file mode 100644
index 068f4a5f..00000000
--- a/ext/bg/js/database.js
+++ /dev/null
@@ -1,327 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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 Database {
- constructor() {
- this._db = null;
- this._isOpening = false;
- }
-
- // Public
-
- async open(databaseName, version, structure) {
- if (this._db !== null) {
- throw new Error('Database already open');
- }
- if (this._isOpening) {
- throw new Error('Already opening');
- }
-
- try {
- this._isOpening = true;
- this._db = await this._open(databaseName, version, (db, transaction, oldVersion) => {
- this._upgrade(db, transaction, oldVersion, structure);
- });
- } finally {
- this._isOpening = false;
- }
- }
-
- close() {
- if (this._db === null) {
- throw new Error('Database is not open');
- }
-
- this._db.close();
- this._db = null;
- }
-
- isOpening() {
- return this._isOpening;
- }
-
- isOpen() {
- return this._db !== null;
- }
-
- transaction(storeNames, mode) {
- if (this._db === null) {
- throw new Error(this._isOpening ? 'Database not ready' : 'Database not open');
- }
- return this._db.transaction(storeNames, mode);
- }
-
- bulkAdd(objectStoreName, items, start, count) {
- return new Promise((resolve, reject) => {
- if (start + count > items.length) {
- count = items.length - start;
- }
-
- if (count <= 0) {
- resolve();
- return;
- }
-
- const end = start + count;
- let completedCount = 0;
- const onError = (e) => reject(e.target.error);
- const onSuccess = () => {
- if (++completedCount >= count) {
- resolve();
- }
- };
-
- const transaction = this.transaction([objectStoreName], 'readwrite');
- const objectStore = transaction.objectStore(objectStoreName);
- for (let i = start; i < end; ++i) {
- const request = objectStore.add(items[i]);
- request.onerror = onError;
- request.onsuccess = onSuccess;
- }
- });
- }
-
- getAll(objectStoreOrIndex, query, resolve, reject) {
- if (typeof objectStoreOrIndex.getAll === 'function') {
- this._getAllFast(objectStoreOrIndex, query, resolve, reject);
- } else {
- this._getAllUsingCursor(objectStoreOrIndex, query, resolve, reject);
- }
- }
-
- getAllKeys(objectStoreOrIndex, query, resolve, reject) {
- if (typeof objectStoreOrIndex.getAll === 'function') {
- this._getAllKeysFast(objectStoreOrIndex, query, resolve, reject);
- } else {
- this._getAllKeysUsingCursor(objectStoreOrIndex, query, resolve, reject);
- }
- }
-
- find(objectStoreName, indexName, query, predicate=null, defaultValue) {
- return new Promise((resolve, reject) => {
- const transaction = this.transaction([objectStoreName], 'readonly');
- const objectStore = transaction.objectStore(objectStoreName);
- const objectStoreOrIndex = indexName !== null ? objectStore.index(indexName) : objectStore;
- const request = objectStoreOrIndex.openCursor(query, 'next');
- request.onerror = (e) => reject(e.target.error);
- request.onsuccess = (e) => {
- const cursor = e.target.result;
- if (cursor) {
- const value = cursor.value;
- if (typeof predicate !== 'function' || predicate(value)) {
- resolve(value);
- } else {
- cursor.continue();
- }
- } else {
- resolve(defaultValue);
- }
- };
- });
- }
-
- bulkCount(targets, resolve, reject) {
- const targetCount = targets.length;
- if (targetCount <= 0) {
- resolve();
- return;
- }
-
- let completedCount = 0;
- const results = new Array(targetCount).fill(null);
-
- const onError = (e) => reject(e.target.error);
- const onSuccess = (e, index) => {
- const count = e.target.result;
- results[index] = count;
- if (++completedCount >= targetCount) {
- resolve(results);
- }
- };
-
- for (let i = 0; i < targetCount; ++i) {
- const index = i;
- const [objectStoreOrIndex, query] = targets[i];
- const request = objectStoreOrIndex.count(query);
- request.onerror = onError;
- request.onsuccess = (e) => onSuccess(e, index);
- }
- }
-
- delete(objectStoreName, key) {
- return new Promise((resolve, reject) => {
- const transaction = this.transaction([objectStoreName], 'readwrite');
- const objectStore = transaction.objectStore(objectStoreName);
- const request = objectStore.delete(key);
- request.onerror = (e) => reject(e.target.error);
- request.onsuccess = () => resolve();
- });
- }
-
- bulkDelete(objectStoreName, indexName, query, filterKeys=null, onProgress=null) {
- return new Promise((resolve, reject) => {
- const transaction = this.transaction([objectStoreName], 'readwrite');
- const objectStore = transaction.objectStore(objectStoreName);
- const objectStoreOrIndex = indexName !== null ? objectStore.index(indexName) : objectStore;
-
- const onGetKeys = (keys) => {
- try {
- if (typeof filterKeys === 'function') {
- keys = filterKeys(keys);
- }
- this._bulkDeleteInternal(objectStore, keys, onProgress, resolve, reject);
- } catch (e) {
- reject(e);
- }
- };
-
- this.getAllKeys(objectStoreOrIndex, query, onGetKeys, reject);
- });
- }
-
- static deleteDatabase(databaseName) {
- return new Promise((resolve, reject) => {
- const request = indexedDB.deleteDatabase(databaseName);
- request.onerror = (e) => reject(e.target.error);
- request.onsuccess = () => resolve();
- request.onblocked = () => reject(new Error('Database deletion blocked'));
- });
- }
-
- // Private
-
- _open(name, version, onUpgradeNeeded) {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(name, version);
-
- request.onupgradeneeded = (event) => {
- try {
- request.transaction.onerror = (e) => reject(e.target.error);
- onUpgradeNeeded(request.result, request.transaction, event.oldVersion, event.newVersion);
- } catch (e) {
- reject(e);
- }
- };
-
- request.onerror = (e) => reject(e.target.error);
- request.onsuccess = () => resolve(request.result);
- });
- }
-
- _upgrade(db, transaction, oldVersion, upgrades) {
- for (const {version, stores} of upgrades) {
- if (oldVersion >= version) { continue; }
-
- for (const [objectStoreName, {primaryKey, indices}] of Object.entries(stores)) {
- const existingObjectStoreNames = transaction.objectStoreNames || db.objectStoreNames;
- const objectStore = (
- this._listContains(existingObjectStoreNames, objectStoreName) ?
- transaction.objectStore(objectStoreName) :
- db.createObjectStore(objectStoreName, primaryKey)
- );
- const existingIndexNames = objectStore.indexNames;
-
- for (const indexName of indices) {
- if (this._listContains(existingIndexNames, indexName)) { continue; }
-
- objectStore.createIndex(indexName, indexName, {});
- }
- }
- }
- }
-
- _listContains(list, value) {
- for (let i = 0, ii = list.length; i < ii; ++i) {
- if (list[i] === value) { return true; }
- }
- return false;
- }
-
- _getAllFast(objectStoreOrIndex, query, resolve, reject) {
- const request = objectStoreOrIndex.getAll(query);
- request.onerror = (e) => reject(e.target.error);
- request.onsuccess = (e) => resolve(e.target.result);
- }
-
- _getAllUsingCursor(objectStoreOrIndex, query, resolve, reject) {
- const results = [];
- const request = objectStoreOrIndex.openCursor(query, 'next');
- request.onerror = (e) => reject(e.target.error);
- request.onsuccess = (e) => {
- const cursor = e.target.result;
- if (cursor) {
- results.push(cursor.value);
- cursor.continue();
- } else {
- resolve(results);
- }
- };
- }
-
- _getAllKeysFast(objectStoreOrIndex, query, resolve, reject) {
- const request = objectStoreOrIndex.getAllKeys(query);
- request.onerror = (e) => reject(e.target.error);
- request.onsuccess = (e) => resolve(e.target.result);
- }
-
- _getAllKeysUsingCursor(objectStoreOrIndex, query, resolve, reject) {
- const results = [];
- const request = objectStoreOrIndex.openKeyCursor(query, 'next');
- request.onerror = (e) => reject(e.target.error);
- request.onsuccess = (e) => {
- const cursor = e.target.result;
- if (cursor) {
- results.push(cursor.primaryKey);
- cursor.continue();
- } else {
- resolve(results);
- }
- };
- }
-
- _bulkDeleteInternal(objectStore, keys, onProgress, resolve, reject) {
- const count = keys.length;
- if (count === 0) {
- resolve();
- return;
- }
-
- let completedCount = 0;
- const hasProgress = (typeof onProgress === 'function');
-
- const onError = (e) => reject(e.target.error);
- const onSuccess = () => {
- ++completedCount;
- if (hasProgress) {
- try {
- onProgress(completedCount, count);
- } catch (e) {
- // NOP
- }
- }
- if (completedCount >= count) {
- resolve();
- }
- };
-
- for (const key of keys) {
- const request = objectStore.delete(key);
- request.onerror = onError;
- request.onsuccess = onSuccess;
- }
- }
-}
diff --git a/ext/bg/js/deinflector.js b/ext/bg/js/deinflector.js
deleted file mode 100644
index 8fee3f01..00000000
--- a/ext/bg/js/deinflector.js
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (C) 2016-2021 Yomichan Authors
- *
- * 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 Deinflector {
- constructor(reasons) {
- this.reasons = Deinflector.normalizeReasons(reasons);
- }
-
- deinflect(source, rawSource) {
- const results = [{
- source,
- rawSource,
- term: source,
- rules: 0,
- reasons: [],
- databaseDefinitions: []
- }];
- for (let i = 0; i < results.length; ++i) {
- const {rules, term, reasons} = results[i];
- for (const [reason, variants] of this.reasons) {
- for (const [kanaIn, kanaOut, rulesIn, rulesOut] of variants) {
- if (
- (rules !== 0 && (rules & rulesIn) === 0) ||
- !term.endsWith(kanaIn) ||
- (term.length - kanaIn.length + kanaOut.length) <= 0
- ) {
- continue;
- }
-
- results.push({
- source,
- rawSource,
- term: term.substring(0, term.length - kanaIn.length) + kanaOut,
- rules: rulesOut,
- reasons: [reason, ...reasons],
- databaseDefinitions: []
- });
- }
- }
- }
- return results;
- }
-
- static normalizeReasons(reasons) {
- const normalizedReasons = [];
- for (const [reason, reasonInfo] of Object.entries(reasons)) {
- const variants = [];
- for (const {kanaIn, kanaOut, rulesIn, rulesOut} of reasonInfo) {
- variants.push([
- kanaIn,
- kanaOut,
- Deinflector.rulesToRuleFlags(rulesIn),
- Deinflector.rulesToRuleFlags(rulesOut)
- ]);
- }
- normalizedReasons.push([reason, variants]);
- }
- return normalizedReasons;
- }
-
- static rulesToRuleFlags(rules) {
- const ruleTypes = Deinflector.ruleTypes;
- let value = 0;
- for (const rule of rules) {
- const ruleBits = ruleTypes.get(rule);
- if (typeof ruleBits === 'undefined') { continue; }
- value |= ruleBits;
- }
- return value;
- }
-}
-
-Deinflector.ruleTypes = new Map([
- ['v1', 0b00000001], // Verb ichidan
- ['v5', 0b00000010], // Verb godan
- ['vs', 0b00000100], // Verb suru
- ['vk', 0b00001000], // Verb kuru
- ['vz', 0b00010000], // Verb zuru
- ['adj-i', 0b00100000], // Adjective i
- ['iru', 0b01000000] // Intermediate -iru endings for progressive or perfect tense
-]);
diff --git a/ext/bg/js/dictionary-database.js b/ext/bg/js/dictionary-database.js
deleted file mode 100644
index b363ed25..00000000
--- a/ext/bg/js/dictionary-database.js
+++ /dev/null
@@ -1,484 +0,0 @@
-/*
- * Copyright (C) 2016-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * Database
- */
-
-class DictionaryDatabase {
- constructor() {
- this._db = new Database();
- this._dbName = 'dict';
- this._schemas = new Map();
- }
-
- // Public
-
- async prepare() {
- await this._db.open(
- this._dbName,
- 60,
- [
- {
- version: 20,
- stores: {
- terms: {
- primaryKey: {keyPath: 'id', autoIncrement: true},
- indices: ['dictionary', 'expression', 'reading']
- },
- kanji: {
- primaryKey: {autoIncrement: true},
- indices: ['dictionary', 'character']
- },
- tagMeta: {
- primaryKey: {autoIncrement: true},
- indices: ['dictionary']
- },
- dictionaries: {
- primaryKey: {autoIncrement: true},
- indices: ['title', 'version']
- }
- }
- },
- {
- version: 30,
- stores: {
- termMeta: {
- primaryKey: {autoIncrement: true},
- indices: ['dictionary', 'expression']
- },
- kanjiMeta: {
- primaryKey: {autoIncrement: true},
- indices: ['dictionary', 'character']
- },
- tagMeta: {
- primaryKey: {autoIncrement: true},
- indices: ['dictionary', 'name']
- }
- }
- },
- {
- version: 40,
- stores: {
- terms: {
- primaryKey: {keyPath: 'id', autoIncrement: true},
- indices: ['dictionary', 'expression', 'reading', 'sequence']
- }
- }
- },
- {
- version: 50,
- stores: {
- terms: {
- primaryKey: {keyPath: 'id', autoIncrement: true},
- indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse']
- }
- }
- },
- {
- version: 60,
- stores: {
- media: {
- primaryKey: {keyPath: 'id', autoIncrement: true},
- indices: ['dictionary', 'path']
- }
- }
- }
- ]
- );
- }
-
- async close() {
- this._db.close();
- }
-
- isPrepared() {
- return this._db.isOpen();
- }
-
- async purge() {
- if (this._db.isOpening()) {
- throw new Error('Cannot purge database while opening');
- }
- if (this._db.isOpen()) {
- this._db.close();
- }
- let result = false;
- try {
- await Database.deleteDatabase(this._dbName);
- result = true;
- } catch (e) {
- yomichan.logError(e);
- }
- await this.prepare();
- return result;
- }
-
- async deleteDictionary(dictionaryName, progressSettings, onProgress) {
- const targets = [
- ['dictionaries', 'title'],
- ['kanji', 'dictionary'],
- ['kanjiMeta', 'dictionary'],
- ['terms', 'dictionary'],
- ['termMeta', 'dictionary'],
- ['tagMeta', 'dictionary'],
- ['media', 'dictionary']
- ];
-
- const {rate} = progressSettings;
- const progressData = {
- count: 0,
- processed: 0,
- storeCount: targets.length,
- storesProcesed: 0
- };
-
- const filterKeys = (keys) => {
- ++progressData.storesProcesed;
- progressData.count += keys.length;
- onProgress(progressData);
- return keys;
- };
- const onProgress2 = () => {
- const processed = progressData.processed + 1;
- progressData.processed = processed;
- if ((processed % rate) === 0 || processed === progressData.count) {
- onProgress(progressData);
- }
- };
-
- const promises = [];
- for (const [objectStoreName, indexName] of targets) {
- const query = IDBKeyRange.only(dictionaryName);
- const promise = this._db.bulkDelete(objectStoreName, indexName, query, filterKeys, onProgress2);
- promises.push(promise);
- }
- await Promise.all(promises);
- }
-
- findTermsBulk(termList, dictionaries, wildcard) {
- return new Promise((resolve, reject) => {
- const results = [];
- const count = termList.length;
- if (count === 0) {
- resolve(results);
- return;
- }
-
- const visited = new Set();
- const useWildcard = !!wildcard;
- const prefixWildcard = wildcard === 'prefix';
-
- const transaction = this._db.transaction(['terms'], 'readonly');
- const terms = transaction.objectStore('terms');
- const index1 = terms.index(prefixWildcard ? 'expressionReverse' : 'expression');
- const index2 = terms.index(prefixWildcard ? 'readingReverse' : 'reading');
-
- const count2 = count * 2;
- let completeCount = 0;
- for (let i = 0; i < count; ++i) {
- const inputIndex = i;
- const term = prefixWildcard ? stringReverse(termList[i]) : termList[i];
- const query = useWildcard ? IDBKeyRange.bound(term, `${term}\uffff`, false, false) : IDBKeyRange.only(term);
-
- const onGetAll = (rows) => {
- for (const row of rows) {
- if (dictionaries.has(row.dictionary) && !visited.has(row.id)) {
- visited.add(row.id);
- results.push(this._createTerm(row, inputIndex));
- }
- }
- if (++completeCount >= count2) {
- resolve(results);
- }
- };
-
- this._db.getAll(index1, query, onGetAll, reject);
- this._db.getAll(index2, query, onGetAll, reject);
- }
- });
- }
-
- findTermsExactBulk(termList, readingList, dictionaries) {
- return new Promise((resolve, reject) => {
- const results = [];
- const count = termList.length;
- if (count === 0) {
- resolve(results);
- return;
- }
-
- const transaction = this._db.transaction(['terms'], 'readonly');
- const terms = transaction.objectStore('terms');
- const index = terms.index('expression');
-
- let completeCount = 0;
- for (let i = 0; i < count; ++i) {
- const inputIndex = i;
- const reading = readingList[i];
- const query = IDBKeyRange.only(termList[i]);
-
- const onGetAll = (rows) => {
- for (const row of rows) {
- if (row.reading === reading && dictionaries.has(row.dictionary)) {
- results.push(this._createTerm(row, inputIndex));
- }
- }
- if (++completeCount >= count) {
- resolve(results);
- }
- };
-
- this._db.getAll(index, query, onGetAll, reject);
- }
- });
- }
-
- findTermsBySequenceBulk(sequenceList, mainDictionary) {
- return new Promise((resolve, reject) => {
- const results = [];
- const count = sequenceList.length;
- if (count === 0) {
- resolve(results);
- return;
- }
-
- const transaction = this._db.transaction(['terms'], 'readonly');
- const terms = transaction.objectStore('terms');
- const index = terms.index('sequence');
-
- let completeCount = 0;
- for (let i = 0; i < count; ++i) {
- const inputIndex = i;
- const query = IDBKeyRange.only(sequenceList[i]);
-
- const onGetAll = (rows) => {
- for (const row of rows) {
- if (row.dictionary === mainDictionary) {
- results.push(this._createTerm(row, inputIndex));
- }
- }
- if (++completeCount >= count) {
- resolve(results);
- }
- };
-
- this._db.getAll(index, query, onGetAll, reject);
- }
- });
- }
-
- findTermMetaBulk(termList, dictionaries) {
- return this._findGenericBulk('termMeta', 'expression', termList, dictionaries, this._createTermMeta.bind(this));
- }
-
- findKanjiBulk(kanjiList, dictionaries) {
- return this._findGenericBulk('kanji', 'character', kanjiList, dictionaries, this._createKanji.bind(this));
- }
-
- findKanjiMetaBulk(kanjiList, dictionaries) {
- return this._findGenericBulk('kanjiMeta', 'character', kanjiList, dictionaries, this._createKanjiMeta.bind(this));
- }
-
- findTagForTitle(name, title) {
- const query = IDBKeyRange.only(name);
- return this._db.find('tagMeta', 'name', query, (row) => (row.dictionary === title), null);
- }
-
- getMedia(targets) {
- return new Promise((resolve, reject) => {
- const count = targets.length;
- const results = new Array(count).fill(null);
- if (count === 0) {
- resolve(results);
- return;
- }
-
- let completeCount = 0;
- const transaction = this._db.transaction(['media'], 'readonly');
- const objectStore = transaction.objectStore('media');
- const index = objectStore.index('path');
-
- for (let i = 0; i < count; ++i) {
- const inputIndex = i;
- const {path, dictionaryName} = targets[i];
- const query = IDBKeyRange.only(path);
-
- const onGetAll = (rows) => {
- for (const row of rows) {
- if (row.dictionary !== dictionaryName) { continue; }
- results[inputIndex] = this._createMedia(row, inputIndex);
- }
- if (++completeCount >= count) {
- resolve(results);
- }
- };
-
- this._db.getAll(index, query, onGetAll, reject);
- }
- });
- }
-
- getDictionaryInfo() {
- return new Promise((resolve, reject) => {
- const transaction = this._db.transaction(['dictionaries'], 'readonly');
- const objectStore = transaction.objectStore('dictionaries');
- this._db.getAll(objectStore, null, resolve, reject);
- });
- }
-
- getDictionaryCounts(dictionaryNames, getTotal) {
- return new Promise((resolve, reject) => {
- const targets = [
- ['kanji', 'dictionary'],
- ['kanjiMeta', 'dictionary'],
- ['terms', 'dictionary'],
- ['termMeta', 'dictionary'],
- ['tagMeta', 'dictionary'],
- ['media', 'dictionary']
- ];
- const objectStoreNames = targets.map(([objectStoreName]) => objectStoreName);
- const transaction = this._db.transaction(objectStoreNames, 'readonly');
- const databaseTargets = targets.map(([objectStoreName, indexName]) => {
- const objectStore = transaction.objectStore(objectStoreName);
- const index = objectStore.index(indexName);
- return {objectStore, index};
- });
-
- const countTargets = [];
- if (getTotal) {
- for (const {objectStore} of databaseTargets) {
- countTargets.push([objectStore, null]);
- }
- }
- for (const dictionaryName of dictionaryNames) {
- const query = IDBKeyRange.only(dictionaryName);
- for (const {index} of databaseTargets) {
- countTargets.push([index, query]);
- }
- }
-
- const onCountComplete = (results) => {
- const resultCount = results.length;
- const targetCount = targets.length;
- const counts = [];
- for (let i = 0; i < resultCount; i += targetCount) {
- const countGroup = {};
- for (let j = 0; j < targetCount; ++j) {
- countGroup[targets[j][0]] = results[i + j];
- }
- counts.push(countGroup);
- }
- const total = getTotal ? counts.shift() : null;
- resolve({total, counts});
- };
-
- this._db.bulkCount(countTargets, onCountComplete, reject);
- });
- }
-
- async dictionaryExists(title) {
- const query = IDBKeyRange.only(title);
- const result = await this._db.find('dictionaries', 'title', query);
- return typeof result !== 'undefined';
- }
-
- bulkAdd(objectStoreName, items, start, count) {
- return this._db.bulkAdd(objectStoreName, items, start, count);
- }
-
- // Private
-
- async _findGenericBulk(objectStoreName, indexName, indexValueList, dictionaries, createResult) {
- return new Promise((resolve, reject) => {
- const results = [];
- const count = indexValueList.length;
- if (count === 0) {
- resolve(results);
- return;
- }
-
- const transaction = this._db.transaction([objectStoreName], 'readonly');
- const terms = transaction.objectStore(objectStoreName);
- const index = terms.index(indexName);
-
- let completeCount = 0;
- for (let i = 0; i < count; ++i) {
- const inputIndex = i;
- const query = IDBKeyRange.only(indexValueList[i]);
-
- const onGetAll = (rows) => {
- for (const row of rows) {
- if (dictionaries.has(row.dictionary)) {
- results.push(createResult(row, inputIndex));
- }
- }
- if (++completeCount >= count) {
- resolve(results);
- }
- };
-
- this._db.getAll(index, query, onGetAll, reject);
- }
- });
- }
-
- _createTerm(row, index) {
- return {
- index,
- expression: row.expression,
- reading: row.reading,
- definitionTags: this._splitField(row.definitionTags || row.tags || ''),
- termTags: this._splitField(row.termTags || ''),
- rules: this._splitField(row.rules),
- glossary: row.glossary,
- score: row.score,
- dictionary: row.dictionary,
- id: row.id,
- sequence: typeof row.sequence === 'undefined' ? -1 : row.sequence
- };
- }
-
- _createKanji(row, index) {
- return {
- index,
- character: row.character,
- onyomi: this._splitField(row.onyomi),
- kunyomi: this._splitField(row.kunyomi),
- tags: this._splitField(row.tags),
- glossary: row.meanings,
- stats: row.stats,
- dictionary: row.dictionary
- };
- }
-
- _createTermMeta({expression, mode, data, dictionary}, index) {
- return {expression, mode, data, dictionary, index};
- }
-
- _createKanjiMeta({character, mode, data, dictionary}, index) {
- return {character, mode, data, dictionary, index};
- }
-
- _createMedia(row, index) {
- return Object.assign({}, row, {index});
- }
-
- _splitField(field) {
- return field.length === 0 ? [] : field.split(' ');
- }
-}
diff --git a/ext/bg/js/dictionary-importer.js b/ext/bg/js/dictionary-importer.js
deleted file mode 100644
index 4cb608db..00000000
--- a/ext/bg/js/dictionary-importer.js
+++ /dev/null
@@ -1,407 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * JSZip
- * JsonSchemaValidator
- * MediaUtility
- */
-
-class DictionaryImporter {
- constructor() {
- this._schemas = new Map();
- this._jsonSchemaValidator = new JsonSchemaValidator();
- this._mediaUtility = new MediaUtility();
- }
-
- async importDictionary(dictionaryDatabase, archiveSource, details, onProgress) {
- if (!dictionaryDatabase) {
- throw new Error('Invalid database');
- }
- if (!dictionaryDatabase.isPrepared()) {
- throw new Error('Database is not ready');
- }
-
- const hasOnProgress = (typeof onProgress === 'function');
-
- // Read archive
- const archive = await JSZip.loadAsync(archiveSource);
-
- // Read and validate index
- const indexFileName = 'index.json';
- const indexFile = archive.files[indexFileName];
- if (!indexFile) {
- throw new Error('No dictionary index found in archive');
- }
-
- const index = JSON.parse(await indexFile.async('string'));
-
- const indexSchema = await this._getSchema('/data/schemas/dictionary-index-schema.json');
- this._validateJsonSchema(index, indexSchema, indexFileName);
-
- const dictionaryTitle = index.title;
- const version = index.format || index.version;
-
- if (!dictionaryTitle || !index.revision) {
- throw new Error('Unrecognized dictionary format');
- }
-
- // Verify database is not already imported
- if (await dictionaryDatabase.dictionaryExists(dictionaryTitle)) {
- throw new Error('Dictionary is already imported');
- }
-
- // Data format converters
- const convertTermBankEntry = (entry) => {
- if (version === 1) {
- const [expression, reading, definitionTags, rules, score, ...glossary] = entry;
- return {expression, reading, definitionTags, rules, score, glossary};
- } else {
- const [expression, reading, definitionTags, rules, score, glossary, sequence, termTags] = entry;
- return {expression, reading, definitionTags, rules, score, glossary, sequence, termTags};
- }
- };
-
- const convertTermMetaBankEntry = (entry) => {
- const [expression, mode, data] = entry;
- return {expression, mode, data};
- };
-
- const convertKanjiBankEntry = (entry) => {
- if (version === 1) {
- const [character, onyomi, kunyomi, tags, ...meanings] = entry;
- return {character, onyomi, kunyomi, tags, meanings};
- } else {
- const [character, onyomi, kunyomi, tags, meanings, stats] = entry;
- return {character, onyomi, kunyomi, tags, meanings, stats};
- }
- };
-
- const convertKanjiMetaBankEntry = (entry) => {
- const [character, mode, data] = entry;
- return {character, mode, data};
- };
-
- const convertTagBankEntry = (entry) => {
- const [name, category, order, notes, score] = entry;
- return {name, category, order, notes, score};
- };
-
- // Archive file reading
- const readFileSequence = async (fileNameFormat, convertEntry, schema) => {
- const results = [];
- for (let i = 1; true; ++i) {
- const fileName = fileNameFormat.replace(/\?/, `${i}`);
- const file = archive.files[fileName];
- if (!file) { break; }
-
- const entries = JSON.parse(await file.async('string'));
- this._validateJsonSchema(entries, schema, fileName);
-
- for (let entry of entries) {
- entry = convertEntry(entry);
- entry.dictionary = dictionaryTitle;
- results.push(entry);
- }
- }
- return results;
- };
-
- // Load schemas
- const dataBankSchemaPaths = this._getDataBankSchemaPaths(version);
- const dataBankSchemas = await Promise.all(dataBankSchemaPaths.map((path) => this._getSchema(path)));
-
- // Load data
- const termList = await readFileSequence('term_bank_?.json', convertTermBankEntry, dataBankSchemas[0]);
- const termMetaList = await readFileSequence('term_meta_bank_?.json', convertTermMetaBankEntry, dataBankSchemas[1]);
- const kanjiList = await readFileSequence('kanji_bank_?.json', convertKanjiBankEntry, dataBankSchemas[2]);
- const kanjiMetaList = await readFileSequence('kanji_meta_bank_?.json', convertKanjiMetaBankEntry, dataBankSchemas[3]);
- const tagList = await readFileSequence('tag_bank_?.json', convertTagBankEntry, dataBankSchemas[4]);
-
- // Old tags
- const indexTagMeta = index.tagMeta;
- if (typeof indexTagMeta === 'object' && indexTagMeta !== null) {
- for (const name of Object.keys(indexTagMeta)) {
- const {category, order, notes, score} = indexTagMeta[name];
- tagList.push({name, category, order, notes, score});
- }
- }
-
- // Prefix wildcard support
- const prefixWildcardsSupported = !!details.prefixWildcardsSupported;
- if (prefixWildcardsSupported) {
- for (const entry of termList) {
- entry.expressionReverse = stringReverse(entry.expression);
- entry.readingReverse = stringReverse(entry.reading);
- }
- }
-
- // Extended data support
- const extendedDataContext = {
- archive,
- media: new Map()
- };
- for (const entry of termList) {
- const glossaryList = entry.glossary;
- for (let i = 0, ii = glossaryList.length; i < ii; ++i) {
- const glossary = glossaryList[i];
- if (typeof glossary !== 'object' || glossary === null) { continue; }
- glossaryList[i] = await this._formatDictionaryTermGlossaryObject(glossary, extendedDataContext, entry);
- }
- }
-
- const media = [...extendedDataContext.media.values()];
-
- // Add dictionary
- const summary = this._createSummary(dictionaryTitle, version, index, {prefixWildcardsSupported});
-
- dictionaryDatabase.bulkAdd('dictionaries', [summary], 0, 1);
-
- // Add data
- const errors = [];
- const total = (
- termList.length +
- termMetaList.length +
- kanjiList.length +
- kanjiMetaList.length +
- tagList.length
- );
- let loadedCount = 0;
- const maxTransactionLength = 1000;
-
- const bulkAdd = async (objectStoreName, entries) => {
- const ii = entries.length;
- for (let i = 0; i < ii; i += maxTransactionLength) {
- const count = Math.min(maxTransactionLength, ii - i);
-
- try {
- await dictionaryDatabase.bulkAdd(objectStoreName, entries, i, count);
- } catch (e) {
- errors.push(e);
- }
-
- loadedCount += count;
- if (hasOnProgress) {
- onProgress(total, loadedCount);
- }
- }
- };
-
- await bulkAdd('terms', termList);
- await bulkAdd('termMeta', termMetaList);
- await bulkAdd('kanji', kanjiList);
- await bulkAdd('kanjiMeta', kanjiMetaList);
- await bulkAdd('tagMeta', tagList);
- await bulkAdd('media', media);
-
- return {result: summary, errors};
- }
-
- _createSummary(dictionaryTitle, version, index, details) {
- const summary = {
- title: dictionaryTitle,
- revision: index.revision,
- sequenced: index.sequenced,
- version
- };
-
- const {author, url, description, attribution} = index;
- if (typeof author === 'string') { summary.author = author; }
- if (typeof url === 'string') { summary.url = url; }
- if (typeof description === 'string') { summary.description = description; }
- if (typeof attribution === 'string') { summary.attribution = attribution; }
-
- Object.assign(summary, details);
-
- return summary;
- }
-
- async _getSchema(fileName) {
- let schemaPromise = this._schemas.get(fileName);
- if (typeof schemaPromise !== 'undefined') {
- return schemaPromise;
- }
-
- schemaPromise = this._fetchJsonAsset(fileName);
- this._schemas.set(fileName, schemaPromise);
- return schemaPromise;
- }
-
- _validateJsonSchema(value, schema, fileName) {
- try {
- this._jsonSchemaValidator.validate(value, schema);
- } catch (e) {
- throw this._formatSchemaError(e, fileName);
- }
- }
-
- _formatSchemaError(e, fileName) {
- const valuePathString = this._getSchemaErrorPathString(e.info.valuePath, 'dictionary');
- const schemaPathString = this._getSchemaErrorPathString(e.info.schemaPath, 'schema');
-
- const e2 = new Error(`Dictionary has invalid data in '${fileName}' for value '${valuePathString}', validated against '${schemaPathString}': ${e.message}`);
- e2.data = e;
-
- return e2;
- }
-
- _getSchemaErrorPathString(infoList, base='') {
- let result = base;
- for (const [part] of infoList) {
- switch (typeof part) {
- case 'string':
- if (result.length > 0) {
- result += '.';
- }
- result += part;
- break;
- case 'number':
- result += `[${part}]`;
- break;
- }
- }
- return result;
- }
-
- _getDataBankSchemaPaths(version) {
- const termBank = (
- version === 1 ?
- '/data/schemas/dictionary-term-bank-v1-schema.json' :
- '/data/schemas/dictionary-term-bank-v3-schema.json'
- );
- const termMetaBank = '/data/schemas/dictionary-term-meta-bank-v3-schema.json';
- const kanjiBank = (
- version === 1 ?
- '/data/schemas/dictionary-kanji-bank-v1-schema.json' :
- '/data/schemas/dictionary-kanji-bank-v3-schema.json'
- );
- const kanjiMetaBank = '/data/schemas/dictionary-kanji-meta-bank-v3-schema.json';
- const tagBank = '/data/schemas/dictionary-tag-bank-v3-schema.json';
-
- return [termBank, termMetaBank, kanjiBank, kanjiMetaBank, tagBank];
- }
-
- async _formatDictionaryTermGlossaryObject(data, context, entry) {
- switch (data.type) {
- case 'text':
- return data.text;
- case 'image':
- return await this._formatDictionaryTermGlossaryImage(data, context, entry);
- default:
- throw new Error(`Unhandled data type: ${data.type}`);
- }
- }
-
- async _formatDictionaryTermGlossaryImage(data, context, entry) {
- const dictionary = entry.dictionary;
- const {path, width: preferredWidth, height: preferredHeight, title, description, pixelated} = data;
- if (context.media.has(path)) {
- // Already exists
- return data;
- }
-
- let errorSource = entry.expression;
- if (entry.reading.length > 0) {
- errorSource += ` (${entry.reading});`;
- }
-
- const file = context.archive.file(path);
- if (file === null) {
- throw new Error(`Could not find image at path ${JSON.stringify(path)} for ${errorSource}`);
- }
-
- const content = await file.async('base64');
- const mediaType = this._mediaUtility.getImageMediaTypeFromFileName(path);
- if (mediaType === null) {
- throw new Error(`Could not determine media type for image at path ${JSON.stringify(path)} for ${errorSource}`);
- }
-
- let image;
- try {
- image = await this._loadImageBase64(mediaType, content);
- } catch (e) {
- throw new Error(`Could not load image at path ${JSON.stringify(path)} for ${errorSource}`);
- }
-
- const width = image.naturalWidth;
- const height = image.naturalHeight;
-
- // Create image data
- const mediaData = {
- dictionary,
- path,
- mediaType,
- width,
- height,
- content
- };
- context.media.set(path, mediaData);
-
- // Create new data
- const newData = {
- type: 'image',
- path,
- width,
- height
- };
- if (typeof preferredWidth === 'number') { newData.preferredWidth = preferredWidth; }
- if (typeof preferredHeight === 'number') { newData.preferredHeight = preferredHeight; }
- if (typeof title === 'string') { newData.title = title; }
- if (typeof description === 'string') { newData.description = description; }
- if (typeof pixelated === 'boolean') { newData.pixelated = pixelated; }
-
- return newData;
- }
-
- async _fetchJsonAsset(url) {
- const response = await fetch(chrome.runtime.getURL(url), {
- method: 'GET',
- mode: 'no-cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer'
- });
- if (!response.ok) {
- throw new Error(`Failed to fetch ${url}: ${response.status}`);
- }
- return await response.json();
- }
-
- /**
- * Attempts to load an image using a base64 encoded content and a media type.
- * @param mediaType The media type for the image content.
- * @param content The binary content for the image, encoded in base64.
- * @returns A Promise which resolves with an HTMLImageElement instance on
- * successful load, otherwise an error is thrown.
- */
- _loadImageBase64(mediaType, content) {
- return new Promise((resolve, reject) => {
- const image = new Image();
- const eventListeners = new EventListenerCollection();
- eventListeners.addEventListener(image, 'load', () => {
- eventListeners.removeAllEventListeners();
- resolve(image);
- }, false);
- eventListeners.addEventListener(image, 'error', () => {
- eventListeners.removeAllEventListeners();
- reject(new Error('Image failed to load'));
- }, false);
- image.src = `data:${mediaType};base64,${content}`;
- });
- }
-}
diff --git a/ext/bg/js/generic-page-main.js b/ext/bg/js/generic-page-main.js
deleted file mode 100644
index db1a770a..00000000
--- a/ext/bg/js/generic-page-main.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * DocumentFocusController
- */
-
-function setupEnvironmentInfo() {
- const {manifest_version: manifestVersion} = chrome.runtime.getManifest();
- document.documentElement.dataset.manifestVersion = `${manifestVersion}`;
-}
-
-(() => {
- const documentFocusController = new DocumentFocusController();
- documentFocusController.prepare();
- document.documentElement.dataset.loaded = 'true';
- setupEnvironmentInfo();
-})();
diff --git a/ext/bg/js/info-main.js b/ext/bg/js/info-main.js
deleted file mode 100644
index 6cf82595..00000000
--- a/ext/bg/js/info-main.js
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * BackupController
- * DocumentFocusController
- * SettingsController
- * api
- */
-
-function getBrowserDisplayName(browser) {
- switch (browser) {
- case 'chrome': return 'Chrome';
- case 'firefox': return 'Firefox';
- case 'firefox-mobile': return 'Firefox for Android';
- case 'edge': return 'Edge';
- case 'edge-legacy': return 'Edge Legacy';
- default: return `${browser}`;
- }
-}
-
-function getOperatingSystemDisplayName(os) {
- switch (os) {
- case 'mac': return 'Mac OS';
- case 'win': return 'Windows';
- case 'android': return 'Android';
- case 'cros': return 'Chrome OS';
- case 'linux': return 'Linux';
- case 'openbsd': return 'Open BSD';
- case 'unknown': return 'Unknown';
- default: return `${os}`;
- }
-}
-
-(async () => {
- try {
- const documentFocusController = new DocumentFocusController();
- documentFocusController.prepare();
-
- const manifest = chrome.runtime.getManifest();
- const language = chrome.i18n.getUILanguage();
-
- api.forwardLogsToBackend();
- await yomichan.prepare();
-
- const {userAgent} = navigator;
- const {name, version} = manifest;
- const {browser, platform: {os}} = await api.getEnvironmentInfo();
-
- const thisVersionLink = document.querySelector('#release-notes-this-version-link');
- thisVersionLink.href = thisVersionLink.dataset.hrefFormat.replace(/\{version\}/g, version);
-
- document.querySelector('#version').textContent = `${name} ${version}`;
- document.querySelector('#browser').textContent = getBrowserDisplayName(browser);
- document.querySelector('#platform').textContent = getOperatingSystemDisplayName(os);
- document.querySelector('#language').textContent = `${language}`;
- document.querySelector('#user-agent').textContent = userAgent;
-
- (async () => {
- let ankiConnectVersion = null;
- try {
- ankiConnectVersion = await api.getAnkiConnectVersion();
- } catch (e) {
- // NOP
- }
-
- document.querySelector('#anki-connect-version').textContent = (ankiConnectVersion !== null ? `${ankiConnectVersion}` : 'Unknown');
- document.querySelector('#anki-connect-version-container').hasError = `${ankiConnectVersion === null}`;
- document.querySelector('#anki-connect-version-unknown-message').hidden = (ankiConnectVersion !== null);
- })();
-
- (async () => {
- let dictionaryInfos;
- try {
- dictionaryInfos = await api.getDictionaryInfo();
- } catch (e) {
- return;
- }
-
- const fragment = document.createDocumentFragment();
- let first = true;
- for (const {title} of dictionaryInfos) {
- if (first) {
- first = false;
- } else {
- fragment.appendChild(document.createTextNode(', '));
- }
-
- const node = document.createElement('span');
- node.className = 'installed-dictionary';
- node.textContent = title;
- fragment.appendChild(node);
- }
-
- document.querySelector('#installed-dictionaries-none').hidden = (dictionaryInfos.length !== 0);
- const container = document.querySelector('#installed-dictionaries');
- container.textContent = '';
- container.appendChild(fragment);
- })();
-
- const settingsController = new SettingsController();
- settingsController.prepare();
-
- const backupController = new BackupController(settingsController, null);
- await backupController.prepare();
-
- await promiseTimeout(100);
-
- document.documentElement.dataset.loaded = 'true';
- } catch (e) {
- yomichan.logError(e);
- }
-})();
diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js
deleted file mode 100644
index 7b6b9c53..00000000
--- a/ext/bg/js/json-schema.js
+++ /dev/null
@@ -1,757 +0,0 @@
-/*
- * Copyright (C) 2019-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * CacheMap
- */
-
-class JsonSchemaProxyHandler {
- constructor(schema, jsonSchemaValidator) {
- this._schema = schema;
- this._jsonSchemaValidator = jsonSchemaValidator;
- }
-
- 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 = this._jsonSchemaValidator.getPropertySchema(this._schema, property, target);
- if (propertySchema === null) {
- return;
- }
-
- const value = target[property];
- return value !== null && typeof value === 'object' ? this._jsonSchemaValidator.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 = this._jsonSchemaValidator.getPropertySchema(this._schema, property, target);
- if (propertySchema === null) {
- throw new Error(`Property ${property} not supported`);
- }
-
- value = clone(value);
-
- this._jsonSchemaValidator.validate(value, propertySchema);
-
- 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');
- }
-}
-
-class JsonSchemaValidator {
- constructor() {
- this._regexCache = new CacheMap(100);
- }
-
- createProxy(target, schema) {
- return new Proxy(target, new JsonSchemaProxyHandler(schema, this));
- }
-
- isValid(value, schema) {
- try {
- this.validate(value, schema);
- return true;
- } catch (e) {
- return false;
- }
- }
-
- validate(value, schema) {
- const info = new JsonSchemaTraversalInfo(value, schema);
- this._validate(value, schema, info);
- }
-
- getValidValueOrDefault(schema, value) {
- const info = new JsonSchemaTraversalInfo(value, schema);
- return this._getValidValueOrDefault(schema, value, info);
- }
-
- getPropertySchema(schema, property, value) {
- return this._getPropertySchema(schema, property, value, null);
- }
-
- clearCache() {
- this._regexCache.clear();
- }
-
- // Private
-
- _getPropertySchema(schema, property, value, path) {
- const type = this._getSchemaOrValueType(schema, value);
- switch (type) {
- case 'object':
- {
- const properties = schema.properties;
- if (this._isObject(properties)) {
- const propertySchema = properties[property];
- if (this._isObject(propertySchema)) {
- if (path !== null) { path.push(['properties', properties], [property, propertySchema]); }
- return propertySchema;
- }
- }
-
- const additionalProperties = schema.additionalProperties;
- if (additionalProperties === false) {
- return null;
- } else if (this._isObject(additionalProperties)) {
- if (path !== null) { path.push(['additionalProperties', additionalProperties]); }
- return additionalProperties;
- } else {
- const result = JsonSchemaValidator.unconstrainedSchema;
- if (path !== null) { path.push([null, result]); }
- return result;
- }
- }
- case 'array':
- {
- const items = schema.items;
- if (this._isObject(items)) {
- return items;
- }
- if (Array.isArray(items)) {
- if (property >= 0 && property < items.length) {
- const propertySchema = items[property];
- if (this._isObject(propertySchema)) {
- if (path !== null) { path.push(['items', items], [property, propertySchema]); }
- return propertySchema;
- }
- }
- }
-
- const additionalItems = schema.additionalItems;
- if (additionalItems === false) {
- return null;
- } else if (this._isObject(additionalItems)) {
- if (path !== null) { path.push(['additionalItems', additionalItems]); }
- return additionalItems;
- } else {
- const result = JsonSchemaValidator.unconstrainedSchema;
- if (path !== null) { path.push([null, result]); }
- return result;
- }
- }
- default:
- return null;
- }
- }
-
- _getSchemaOrValueType(schema, value) {
- const type = schema.type;
-
- if (Array.isArray(type)) {
- if (typeof value !== 'undefined') {
- const valueType = this._getValueType(value);
- if (type.indexOf(valueType) >= 0) {
- return valueType;
- }
- }
- return null;
- }
-
- if (typeof type === 'undefined') {
- if (typeof value !== 'undefined') {
- return this._getValueType(value);
- }
- return null;
- }
-
- return type;
- }
-
- _validate(value, schema, info) {
- this._validateSingleSchema(value, schema, info);
- this._validateConditional(value, schema, info);
- this._validateAllOf(value, schema, info);
- this._validateAnyOf(value, schema, info);
- this._validateOneOf(value, schema, info);
- this._validateNoneOf(value, schema, info);
- }
-
- _validateConditional(value, schema, info) {
- const ifSchema = schema.if;
- if (!this._isObject(ifSchema)) { return; }
-
- let okay = true;
- info.schemaPush('if', ifSchema);
- try {
- this._validate(value, ifSchema, info);
- } catch (e) {
- okay = false;
- }
- info.schemaPop();
-
- const nextSchema = okay ? schema.then : schema.else;
- if (this._isObject(nextSchema)) {
- info.schemaPush(okay ? 'then' : 'else', nextSchema);
- this._validate(value, nextSchema, info);
- info.schemaPop();
- }
- }
-
- _validateAllOf(value, schema, info) {
- const subSchemas = schema.allOf;
- if (!Array.isArray(subSchemas)) { return; }
-
- info.schemaPush('allOf', subSchemas);
- for (let i = 0; i < subSchemas.length; ++i) {
- const subSchema = subSchemas[i];
- info.schemaPush(i, subSchema);
- this._validate(value, subSchema, info);
- info.schemaPop();
- }
- info.schemaPop();
- }
-
- _validateAnyOf(value, schema, info) {
- const subSchemas = schema.anyOf;
- if (!Array.isArray(subSchemas)) { return; }
-
- info.schemaPush('anyOf', subSchemas);
- for (let i = 0; i < subSchemas.length; ++i) {
- const subSchema = subSchemas[i];
- info.schemaPush(i, subSchema);
- try {
- this._validate(value, subSchema, info);
- return;
- } catch (e) {
- // NOP
- }
- info.schemaPop();
- }
-
- throw new JsonSchemaValidationError('0 anyOf schemas matched', value, schema, info);
- // info.schemaPop(); // Unreachable
- }
-
- _validateOneOf(value, schema, info) {
- const subSchemas = schema.oneOf;
- if (!Array.isArray(subSchemas)) { return; }
-
- info.schemaPush('oneOf', subSchemas);
- let count = 0;
- for (let i = 0; i < subSchemas.length; ++i) {
- const subSchema = subSchemas[i];
- info.schemaPush(i, subSchema);
- try {
- this._validate(value, subSchema, info);
- ++count;
- } catch (e) {
- // NOP
- }
- info.schemaPop();
- }
-
- if (count !== 1) {
- throw new JsonSchemaValidationError(`${count} oneOf schemas matched`, value, schema, info);
- }
-
- info.schemaPop();
- }
-
- _validateNoneOf(value, schema, info) {
- const subSchemas = schema.not;
- if (!Array.isArray(subSchemas)) { return; }
-
- info.schemaPush('not', subSchemas);
- for (let i = 0; i < subSchemas.length; ++i) {
- const subSchema = subSchemas[i];
- info.schemaPush(i, subSchema);
- try {
- this._validate(value, subSchema, info);
- } catch (e) {
- info.schemaPop();
- continue;
- }
- throw new JsonSchemaValidationError(`not[${i}] schema matched`, value, schema, info);
- }
- info.schemaPop();
- }
-
- _validateSingleSchema(value, schema, info) {
- const type = this._getValueType(value);
- const schemaType = schema.type;
- if (!this._isValueTypeAny(value, type, schemaType)) {
- throw new JsonSchemaValidationError(`Value type ${type} does not match schema type ${schemaType}`, value, schema, info);
- }
-
- const schemaConst = schema.const;
- if (typeof schemaConst !== 'undefined' && !this._valuesAreEqual(value, schemaConst)) {
- throw new JsonSchemaValidationError('Invalid constant value', value, schema, info);
- }
-
- const schemaEnum = schema.enum;
- if (Array.isArray(schemaEnum) && !this._valuesAreEqualAny(value, schemaEnum)) {
- throw new JsonSchemaValidationError('Invalid enum value', value, schema, info);
- }
-
- switch (type) {
- case 'number':
- this._validateNumber(value, schema, info);
- break;
- case 'string':
- this._validateString(value, schema, info);
- break;
- case 'array':
- this._validateArray(value, schema, info);
- break;
- case 'object':
- this._validateObject(value, schema, info);
- break;
- }
- }
-
- _validateNumber(value, schema, info) {
- const multipleOf = schema.multipleOf;
- if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {
- throw new JsonSchemaValidationError(`Number is not a multiple of ${multipleOf}`, value, schema, info);
- }
-
- const minimum = schema.minimum;
- if (typeof minimum === 'number' && value < minimum) {
- throw new JsonSchemaValidationError(`Number is less than ${minimum}`, value, schema, info);
- }
-
- const exclusiveMinimum = schema.exclusiveMinimum;
- if (typeof exclusiveMinimum === 'number' && value <= exclusiveMinimum) {
- throw new JsonSchemaValidationError(`Number is less than or equal to ${exclusiveMinimum}`, value, schema, info);
- }
-
- const maximum = schema.maximum;
- if (typeof maximum === 'number' && value > maximum) {
- throw new JsonSchemaValidationError(`Number is greater than ${maximum}`, value, schema, info);
- }
-
- const exclusiveMaximum = schema.exclusiveMaximum;
- if (typeof exclusiveMaximum === 'number' && value >= exclusiveMaximum) {
- throw new JsonSchemaValidationError(`Number is greater than or equal to ${exclusiveMaximum}`, value, schema, info);
- }
- }
-
- _validateString(value, schema, info) {
- const minLength = schema.minLength;
- if (typeof minLength === 'number' && value.length < minLength) {
- throw new JsonSchemaValidationError('String length too short', value, schema, info);
- }
-
- const maxLength = schema.maxLength;
- if (typeof maxLength === 'number' && value.length > maxLength) {
- throw new JsonSchemaValidationError('String length too long', value, schema, info);
- }
-
- const pattern = schema.pattern;
- if (typeof pattern === 'string') {
- let patternFlags = schema.patternFlags;
- if (typeof patternFlags !== 'string') { patternFlags = ''; }
-
- let regex;
- try {
- regex = this._getRegex(pattern, patternFlags);
- } catch (e) {
- throw new JsonSchemaValidationError(`Pattern is invalid (${e.message})`, value, schema, info);
- }
-
- if (!regex.test(value)) {
- throw new JsonSchemaValidationError('Pattern match failed', value, schema, info);
- }
- }
- }
-
- _validateArray(value, schema, info) {
- const minItems = schema.minItems;
- if (typeof minItems === 'number' && value.length < minItems) {
- throw new JsonSchemaValidationError('Array length too short', value, schema, info);
- }
-
- const maxItems = schema.maxItems;
- if (typeof maxItems === 'number' && value.length > maxItems) {
- throw new JsonSchemaValidationError('Array length too long', value, schema, info);
- }
-
- this._validateArrayContains(value, schema, info);
-
- for (let i = 0, ii = value.length; i < ii; ++i) {
- const schemaPath = [];
- const propertySchema = this._getPropertySchema(schema, i, value, schemaPath);
- if (propertySchema === null) {
- throw new JsonSchemaValidationError(`No schema found for array[${i}]`, value, schema, info);
- }
-
- const propertyValue = value[i];
-
- for (const [p, s] of schemaPath) { info.schemaPush(p, s); }
- info.valuePush(i, propertyValue);
- this._validate(propertyValue, propertySchema, info);
- info.valuePop();
- for (let j = 0, jj = schemaPath.length; j < jj; ++j) { info.schemaPop(); }
- }
- }
-
- _validateArrayContains(value, schema, info) {
- const containsSchema = schema.contains;
- if (!this._isObject(containsSchema)) { return; }
-
- info.schemaPush('contains', containsSchema);
- for (let i = 0, ii = value.length; i < ii; ++i) {
- const propertyValue = value[i];
- info.valuePush(i, propertyValue);
- try {
- this._validate(propertyValue, containsSchema, info);
- info.schemaPop();
- return;
- } catch (e) {
- // NOP
- }
- info.valuePop();
- }
- throw new JsonSchemaValidationError('contains schema didn\'t match', value, schema, info);
- }
-
- _validateObject(value, schema, info) {
- const properties = new Set(Object.getOwnPropertyNames(value));
-
- const required = schema.required;
- if (Array.isArray(required)) {
- for (const property of required) {
- if (!properties.has(property)) {
- throw new JsonSchemaValidationError(`Missing property ${property}`, value, schema, info);
- }
- }
- }
-
- const minProperties = schema.minProperties;
- if (typeof minProperties === 'number' && properties.length < minProperties) {
- throw new JsonSchemaValidationError('Not enough object properties', value, schema, info);
- }
-
- const maxProperties = schema.maxProperties;
- if (typeof maxProperties === 'number' && properties.length > maxProperties) {
- throw new JsonSchemaValidationError('Too many object properties', value, schema, info);
- }
-
- for (const property of properties) {
- const schemaPath = [];
- const propertySchema = this._getPropertySchema(schema, property, value, schemaPath);
- if (propertySchema === null) {
- throw new JsonSchemaValidationError(`No schema found for ${property}`, value, schema, info);
- }
-
- const propertyValue = value[property];
-
- for (const [p, s] of schemaPath) { info.schemaPush(p, s); }
- info.valuePush(property, propertyValue);
- this._validate(propertyValue, propertySchema, info);
- info.valuePop();
- for (let i = 0; i < schemaPath.length; ++i) { info.schemaPop(); }
- }
- }
-
- _isValueTypeAny(value, type, schemaTypes) {
- if (typeof schemaTypes === 'string') {
- return this._isValueType(value, type, schemaTypes);
- } else if (Array.isArray(schemaTypes)) {
- for (const schemaType of schemaTypes) {
- if (this._isValueType(value, type, schemaType)) {
- return true;
- }
- }
- return false;
- }
- return true;
- }
-
- _isValueType(value, type, schemaType) {
- return (
- type === schemaType ||
- (schemaType === 'integer' && Math.floor(value) === value)
- );
- }
-
- _getValueType(value) {
- const type = typeof value;
- if (type === 'object') {
- if (value === null) { return 'null'; }
- if (Array.isArray(value)) { return 'array'; }
- }
- return type;
- }
-
- _valuesAreEqualAny(value1, valueList) {
- for (const value2 of valueList) {
- if (this._valuesAreEqual(value1, value2)) {
- return true;
- }
- }
- return false;
- }
-
- _valuesAreEqual(value1, value2) {
- return value1 === value2;
- }
-
- _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;
- }
-
- _getDefaultSchemaValue(schema) {
- const schemaType = schema.type;
- const schemaDefault = schema.default;
- return (
- typeof schemaDefault !== 'undefined' &&
- this._isValueTypeAny(schemaDefault, this._getValueType(schemaDefault), schemaType) ?
- clone(schemaDefault) :
- this._getDefaultTypeValue(schemaType)
- );
- }
-
- _getValidValueOrDefault(schema, value, info) {
- let type = this._getValueType(value);
- if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, schema.type)) {
- value = this._getDefaultSchemaValue(schema);
- type = this._getValueType(value);
- }
-
- switch (type) {
- case 'object':
- value = this._populateObjectDefaults(value, schema, info);
- break;
- case 'array':
- value = this._populateArrayDefaults(value, schema, info);
- break;
- default:
- if (!this.isValid(value, schema)) {
- const schemaDefault = this._getDefaultSchemaValue(schema);
- if (this.isValid(schemaDefault, schema)) {
- value = schemaDefault;
- }
- }
- break;
- }
-
- return value;
- }
-
- _populateObjectDefaults(value, schema, info) {
- 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 = this._getPropertySchema(schema, property, value, null);
- if (propertySchema === null) { continue; }
- info.valuePush(property, value);
- info.schemaPush(property, propertySchema);
- const hasValue = Object.prototype.hasOwnProperty.call(value, property);
- value[property] = this._getValidValueOrDefault(propertySchema, hasValue ? value[property] : void 0, info);
- info.schemaPop();
- info.valuePop();
- }
- }
-
- for (const property of properties) {
- const propertySchema = this._getPropertySchema(schema, property, value, null);
- if (propertySchema === null) {
- Reflect.deleteProperty(value, property);
- } else {
- info.valuePush(property, value);
- info.schemaPush(property, propertySchema);
- value[property] = this._getValidValueOrDefault(propertySchema, value[property], info);
- info.schemaPop();
- info.valuePop();
- }
- }
-
- return value;
- }
-
- _populateArrayDefaults(value, schema, info) {
- for (let i = 0, ii = value.length; i < ii; ++i) {
- const propertySchema = this._getPropertySchema(schema, i, value, null);
- if (propertySchema === null) { continue; }
- info.valuePush(i, value);
- info.schemaPush(i, propertySchema);
- value[i] = this._getValidValueOrDefault(propertySchema, value[i], info);
- info.schemaPop();
- info.valuePop();
- }
-
- const minItems = schema.minItems;
- if (typeof minItems === 'number' && value.length < minItems) {
- for (let i = value.length; i < minItems; ++i) {
- const propertySchema = this._getPropertySchema(schema, i, value, null);
- if (propertySchema === null) { break; }
- info.valuePush(i, value);
- info.schemaPush(i, propertySchema);
- const item = this._getValidValueOrDefault(propertySchema, void 0, info);
- info.schemaPop();
- info.valuePop();
- value.push(item);
- }
- }
-
- const maxItems = schema.maxItems;
- if (typeof maxItems === 'number' && value.length > maxItems) {
- value.splice(maxItems, value.length - maxItems);
- }
-
- return value;
- }
-
- _isObject(value) {
- return typeof value === 'object' && value !== null && !Array.isArray(value);
- }
-
- _getRegex(pattern, flags) {
- const key = `${flags}:${pattern}`;
- let regex = this._regexCache.get(key);
- if (typeof regex === 'undefined') {
- regex = new RegExp(pattern, flags);
- this._regexCache.set(key, regex);
- }
- return regex;
- }
-}
-
-Object.defineProperty(JsonSchemaValidator, 'unconstrainedSchema', {
- value: Object.freeze({}),
- configurable: false,
- enumerable: true,
- writable: false
-});
-
-class JsonSchemaTraversalInfo {
- constructor(value, schema) {
- this.valuePath = [];
- this.schemaPath = [];
- this.valuePush(null, value);
- this.schemaPush(null, schema);
- }
-
- valuePush(path, value) {
- this.valuePath.push([path, value]);
- }
-
- valuePop() {
- this.valuePath.pop();
- }
-
- schemaPush(path, schema) {
- this.schemaPath.push([path, schema]);
- }
-
- schemaPop() {
- this.schemaPath.pop();
- }
-}
-
-class JsonSchemaValidationError extends Error {
- constructor(message, value, schema, info) {
- super(message);
- this.value = value;
- this.schema = schema;
- this.info = info;
- }
-}
diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js
deleted file mode 100644
index 4eff2927..00000000
--- a/ext/bg/js/mecab.js
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright (C) 2019-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/**
- * This class is used to connect Yomichan to a native component that is
- * used to parse text into individual terms.
- */
-class Mecab {
- /**
- * Creates a new instance of the class.
- */
- constructor() {
- this._port = null;
- this._sequence = 0;
- this._invocations = new Map();
- this._eventListeners = new EventListenerCollection();
- this._timeout = 5000;
- this._version = 1;
- this._remoteVersion = null;
- this._enabled = false;
- this._setupPortPromise = null;
- }
-
- /**
- * Returns whether or not the component is enabled.
- */
- isEnabled() {
- return this._enabled;
- }
-
- /**
- * Changes whether or not the component connection is enabled.
- * @param enabled A boolean indicating whether or not the component should be enabled.
- */
- setEnabled(enabled) {
- this._enabled = !!enabled;
- if (!this._enabled && this._port !== null) {
- this._clearPort();
- }
- }
-
- /**
- * Disconnects the current port, but does not disable future connections.
- */
- disconnect() {
- if (this._port !== null) {
- this._clearPort();
- }
- }
-
- /**
- * Returns whether or not the connection to the native application is active.
- * @returns `true` if the connection is active, `false` otherwise.
- */
- isConnected() {
- return (this._port !== null);
- }
-
- /**
- * Returns whether or not any invocation is currently active.
- * @returns `true` if an invocation is active, `false` otherwise.
- */
- isActive() {
- return (this._invocations.size > 0);
- }
-
- /**
- * Gets the local API version being used.
- * @returns An integer representing the API version that Yomichan uses.
- */
- getLocalVersion() {
- return this._version;
- }
-
- /**
- * Gets the version of the MeCab component.
- * @returns The version of the MeCab component, or `null` if the component was not found.
- */
- async getVersion() {
- try {
- await this._setupPort();
- } catch (e) {
- // NOP
- }
- return this._remoteVersion;
- }
-
- /**
- * Parses a string of Japanese text into arrays of lines and terms.
- *
- * Return value format:
- * ```js
- * [
- * {
- * name: (string),
- * lines: [
- * {expression: (string), reading: (string), source: (string)},
- * ...
- * ]
- * },
- * ...
- * ]
- * ```
- * @param text The string to parse.
- * @returns A collection of parsing results of the text.
- */
- async parseText(text) {
- await this._setupPort();
- const rawResults = await this._invoke('parse_text', {text});
- return this._convertParseTextResults(rawResults);
- }
-
- // Private
-
- _onMessage({sequence, data}) {
- const invocation = this._invocations.get(sequence);
- if (typeof invocation === 'undefined') { return; }
-
- const {resolve, timer} = invocation;
- clearTimeout(timer);
- resolve(data);
- this._invocations.delete(sequence);
- }
-
- _onDisconnect() {
- if (this._port === null) { return; }
- const e = chrome.runtime.lastError;
- const error = new Error(e ? e.message : 'MeCab disconnected');
- for (const {reject, timer} of this._invocations.values()) {
- clearTimeout(timer);
- reject(error);
- }
- this._clearPort();
- }
-
- _invoke(action, params) {
- return new Promise((resolve, reject) => {
- if (this._port === null) {
- reject(new Error('Port disconnected'));
- }
-
- const sequence = this._sequence++;
-
- const timer = setTimeout(() => {
- this._invocations.delete(sequence);
- reject(new Error(`MeCab invoke timed out after ${this._timeout}ms`));
- }, this._timeout);
-
- this._invocations.set(sequence, {resolve, reject, timer}, this._timeout);
-
- this._port.postMessage({action, params, sequence});
- });
- }
-
- _convertParseTextResults(rawResults) {
- const results = [];
- for (const [name, rawLines] of Object.entries(rawResults)) {
- const lines = [];
- for (const rawLine of rawLines) {
- const line = [];
- for (let {expression, reading, source} of rawLine) {
- if (typeof expression !== 'string') { expression = ''; }
- if (typeof reading !== 'string') { reading = ''; }
- if (typeof source !== 'string') { source = ''; }
- line.push({expression, reading, source});
- }
- lines.push(line);
- }
- results.push({name, lines});
- }
- return results;
- }
-
- async _setupPort() {
- if (!this._enabled) {
- throw new Error('MeCab not enabled');
- }
- if (this._setupPortPromise === null) {
- this._setupPortPromise = this._setupPort2();
- }
- try {
- await this._setupPortPromise;
- } catch (e) {
- throw new Error(e.message);
- }
- }
-
- async _setupPort2() {
- const port = chrome.runtime.connectNative('yomichan_mecab');
- this._eventListeners.addListener(port.onMessage, this._onMessage.bind(this));
- this._eventListeners.addListener(port.onDisconnect, this._onDisconnect.bind(this));
- this._port = port;
-
- try {
- const {version} = await this._invoke('get_version', {});
- this._remoteVersion = version;
- if (version !== this._version) {
- throw new Error(`Unsupported MeCab native messenger version ${version}. Yomichan supports version ${this._version}.`);
- }
- } catch (e) {
- if (this._port === port) {
- this._clearPort();
- }
- throw e;
- }
- }
-
- _clearPort() {
- this._port.disconnect();
- this._port = null;
- this._invocations.clear();
- this._eventListeners.removeAllEventListeners();
- this._sequence = 0;
- this._setupPortPromise = null;
- }
-}
diff --git a/ext/bg/js/media-utility.js b/ext/bg/js/media-utility.js
deleted file mode 100644
index b4fbe04d..00000000
--- a/ext/bg/js/media-utility.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/**
- * MediaUtility is a class containing helper methods related to media processing.
- */
-class MediaUtility {
- /**
- * Gets the file extension of a file path. URL search queries and hash
- * fragments are not handled.
- * @param path The path to the file.
- * @returns The file extension, including the '.', or an empty string
- * if there is no file extension.
- */
- getFileNameExtension(path) {
- const match = /\.[^./\\]*$/.exec(path);
- return match !== null ? match[0] : '';
- }
-
- /**
- * Gets an image file's media type using a file path.
- * @param path The path to the file.
- * @returns The media type string if it can be determined from the file path,
- * otherwise null.
- */
- getImageMediaTypeFromFileName(path) {
- switch (this.getFileNameExtension(path).toLowerCase()) {
- case '.apng':
- return 'image/apng';
- case '.bmp':
- return 'image/bmp';
- case '.gif':
- return 'image/gif';
- case '.ico':
- case '.cur':
- return 'image/x-icon';
- case '.jpg':
- case '.jpeg':
- case '.jfif':
- case '.pjpeg':
- case '.pjp':
- return 'image/jpeg';
- case '.png':
- return 'image/png';
- case '.svg':
- return 'image/svg+xml';
- case '.tif':
- case '.tiff':
- return 'image/tiff';
- case '.webp':
- return 'image/webp';
- default:
- return null;
- }
- }
-
- /**
- * Gets the file extension for a corresponding media type.
- * @param mediaType The media type to use.
- * @returns A file extension including the dot for the media type,
- * otherwise null.
- */
- getFileExtensionFromImageMediaType(mediaType) {
- switch (mediaType) {
- case 'image/apng':
- return '.apng';
- case 'image/bmp':
- return '.bmp';
- case 'image/gif':
- return '.gif';
- case 'image/x-icon':
- return '.ico';
- case 'image/jpeg':
- return '.jpeg';
- case 'image/png':
- return '.png';
- case 'image/svg+xml':
- return '.svg';
- case 'image/tiff':
- return '.tiff';
- case 'image/webp':
- return '.webp';
- default:
- return null;
- }
- }
-
- /**
- * Gets the file extension for a corresponding media type.
- * @param mediaType The media type to use.
- * @returns A file extension including the dot for the media type,
- * otherwise null.
- */
- getFileExtensionFromAudioMediaType(mediaType) {
- switch (mediaType) {
- case 'audio/mpeg':
- case 'audio/mp3':
- return '.mp3';
- case 'audio/mp4':
- return '.mp4';
- case 'audio/ogg':
- case 'audio/vorbis':
- return '.ogg';
- case 'audio/vnd.wav':
- case 'audio/wave':
- case 'audio/wav':
- case 'audio/x-wav':
- case 'audio/x-pn-wav':
- return '.wav';
- case 'audio/flac':
- return '.flac';
- case 'audio/webm':
- return '.webm';
- default:
- return null;
- }
- }
-}
diff --git a/ext/bg/js/native-simple-dom-parser.js b/ext/bg/js/native-simple-dom-parser.js
deleted file mode 100644
index 27dadec0..00000000
--- a/ext/bg/js/native-simple-dom-parser.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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 NativeSimpleDOMParser {
- constructor(content) {
- this._document = new DOMParser().parseFromString(content, 'text/html');
- }
-
- getElementById(id, root=null) {
- return (root || this._document).querySelector(`[id='${id}']`);
- }
-
- getElementByTagName(tagName, root=null) {
- return (root || this._document).querySelector(tagName);
- }
-
- getElementsByTagName(tagName, root=null) {
- return [...(root || this._document).querySelectorAll(tagName)];
- }
-
- getElementsByClassName(className, root=null) {
- return [...(root || this._document).querySelectorAll(`.${className}`)];
- }
-
- getAttribute(element, attribute) {
- return element.hasAttribute(attribute) ? element.getAttribute(attribute) : null;
- }
-
- getTextContent(element) {
- return element.textContent;
- }
-
- static isSupported() {
- return typeof DOMParser !== 'undefined';
- }
-}
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
deleted file mode 100644
index 1105dfed..00000000
--- a/ext/bg/js/options.js
+++ /dev/null
@@ -1,739 +0,0 @@
-/*
- * Copyright (C) 2016-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * JsonSchemaValidator
- * TemplatePatcher
- */
-
-class OptionsUtil {
- constructor() {
- this._schemaValidator = new JsonSchemaValidator();
- this._templatePatcher = null;
- this._optionsSchema = null;
- }
-
- async prepare() {
- this._optionsSchema = await this._fetchAsset('/data/schemas/options-schema.json', true);
- }
-
- async update(options) {
- // Invalid options
- if (!isObject(options)) {
- options = {};
- }
-
- // Check for legacy options
- let defaultProfileOptions = {};
- if (!Array.isArray(options.profiles)) {
- defaultProfileOptions = options;
- options = {};
- }
-
- // Ensure profiles is an array
- if (!Array.isArray(options.profiles)) {
- options.profiles = [];
- }
-
- // Remove invalid profiles
- const profiles = options.profiles;
- for (let i = profiles.length - 1; i >= 0; --i) {
- if (!isObject(profiles[i])) {
- profiles.splice(i, 1);
- }
- }
-
- // Require at least one profile
- if (profiles.length === 0) {
- profiles.push({
- name: 'Default',
- options: defaultProfileOptions,
- conditionGroups: []
- });
- }
-
- // Ensure profileCurrent is valid
- const profileCurrent = options.profileCurrent;
- if (!(
- typeof profileCurrent === 'number' &&
- Number.isFinite(profileCurrent) &&
- Math.floor(profileCurrent) === profileCurrent &&
- profileCurrent >= 0 &&
- profileCurrent < profiles.length
- )) {
- options.profileCurrent = 0;
- }
-
- // Version
- if (typeof options.version !== 'number') {
- options.version = 0;
- }
-
- // Generic updates
- options = await this._applyUpdates(options, this._getVersionUpdates());
-
- // Validation
- options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema, options);
-
- // Result
- return options;
- }
-
- async load() {
- let options;
- try {
- const optionsStr = await new Promise((resolve, reject) => {
- chrome.storage.local.get(['options'], (store) => {
- const error = chrome.runtime.lastError;
- if (error) {
- reject(new Error(error.message));
- } else {
- resolve(store.options);
- }
- });
- });
- options = JSON.parse(optionsStr);
- } catch (e) {
- // NOP
- }
-
- if (typeof options !== 'undefined') {
- options = await this.update(options);
- } else {
- options = this.getDefault();
- }
-
- return options;
- }
-
- save(options) {
- return new Promise((resolve, reject) => {
- chrome.storage.local.set({options: JSON.stringify(options)}, () => {
- const error = chrome.runtime.lastError;
- if (error) {
- reject(new Error(error.message));
- } else {
- resolve();
- }
- });
- });
- }
-
- getDefault() {
- const optionsVersion = this._getVersionUpdates().length;
- const options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema);
- options.version = optionsVersion;
- return options;
- }
-
- createValidatingProxy(options) {
- return this._schemaValidator.createProxy(options, this._optionsSchema);
- }
-
- validate(options) {
- return this._schemaValidator.validate(options, this._optionsSchema);
- }
-
- // Legacy profile updating
-
- _legacyProfileUpdateGetUpdates() {
- return [
- null,
- null,
- null,
- null,
- (options) => {
- options.general.audioSource = options.general.audioPlayback ? 'jpod101' : 'disabled';
- },
- (options) => {
- options.general.showGuide = false;
- },
- (options) => {
- options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none';
- },
- (options) => {
- options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split';
- options.anki.fieldTemplates = null;
- },
- (options) => {
- if (this._getStringHashCode(options.anki.fieldTemplates) === 1285806040) {
- options.anki.fieldTemplates = null;
- }
- },
- (options) => {
- if (this._getStringHashCode(options.anki.fieldTemplates) === -250091611) {
- options.anki.fieldTemplates = null;
- }
- },
- (options) => {
- const oldAudioSource = options.general.audioSource;
- const disabled = oldAudioSource === 'disabled';
- options.audio.enabled = !disabled;
- options.audio.volume = options.general.audioVolume;
- options.audio.autoPlay = options.general.autoPlayAudio;
- options.audio.sources = [disabled ? 'jpod101' : oldAudioSource];
-
- 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 (this._getStringHashCode(options.anki.fieldTemplates) === 1444379824) {
- options.anki.fieldTemplates = null;
- }
- },
- (options) => {
- // Version 13 changes:
- // Default anki field tempaltes updated to include {document-title}.
- let fieldTemplates = options.anki.fieldTemplates;
- if (typeof fieldTemplates === 'string') {
- fieldTemplates += '\n\n{{#*inline "document-title"}}\n {{~context.document.title~}}\n{{/inline}}';
- options.anki.fieldTemplates = fieldTemplates;
- }
- },
- (options) => {
- // Version 14 changes:
- // Changed template for Anki audio and tags.
- let fieldTemplates = options.anki.fieldTemplates;
- if (typeof fieldTemplates !== 'string') { return; }
-
- const replacements = [
- [
- '{{#*inline "audio"}}{{/inline}}',
- '{{#*inline "audio"}}\n {{~#if definition.audioFileName~}}\n [sound:{{definition.audioFileName}}]\n {{~/if~}}\n{{/inline}}'
- ],
- [
- '{{#*inline "tags"}}\n {{~#each definition.definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}}\n{{/inline}}',
- '{{#*inline "tags"}}\n {{~#mergeTags definition group merge}}{{this}}{{/mergeTags~}}\n{{/inline}}'
- ]
- ];
-
- for (const [pattern, replacement] of replacements) {
- let replaced = false;
- fieldTemplates = fieldTemplates.replace(new RegExp(escapeRegExp(pattern), 'g'), () => {
- replaced = true;
- return replacement;
- });
-
- if (!replaced) {
- fieldTemplates += '\n\n' + replacement;
- }
- }
-
- options.anki.fieldTemplates = fieldTemplates;
- }
- ];
- }
-
- _legacyProfileUpdateGetDefaults() {
- return {
- general: {
- enable: true,
- enableClipboardPopups: false,
- resultOutputMode: 'group',
- debugInfo: false,
- maxResults: 32,
- showAdvanced: false,
- popupDisplayMode: 'default',
- popupWidth: 400,
- popupHeight: 250,
- popupHorizontalOffset: 0,
- popupVerticalOffset: 10,
- popupHorizontalOffset2: 10,
- popupVerticalOffset2: 0,
- popupHorizontalTextPosition: 'below',
- popupVerticalTextPosition: 'before',
- popupScalingFactor: 1,
- popupScaleRelativeToPageZoom: false,
- popupScaleRelativeToVisualViewport: true,
- showGuide: true,
- compactTags: false,
- compactGlossaries: false,
- mainDictionary: '',
- popupTheme: 'default',
- popupOuterTheme: 'default',
- customPopupCss: '',
- customPopupOuterCss: '',
- enableWanakana: true,
- enableClipboardMonitor: false,
- showPitchAccentDownstepNotation: true,
- showPitchAccentPositionNotation: true,
- showPitchAccentGraph: false,
- showIframePopupsInRootFrame: false,
- useSecurePopupFrameUrl: true,
- usePopupShadowDom: true
- },
-
- audio: {
- enabled: true,
- sources: ['jpod101'],
- volume: 100,
- autoPlay: false,
- customSourceUrl: '',
- textToSpeechVoice: ''
- },
-
- scanning: {
- middleMouse: true,
- touchInputEnabled: true,
- selectText: true,
- alphanumeric: true,
- autoHideResults: false,
- delay: 20,
- length: 10,
- modifier: 'shift',
- deepDomScan: false,
- popupNestingMaxDepth: 0,
- enablePopupSearch: false,
- enableOnPopupExpressions: false,
- enableOnSearchPage: true,
- enableSearchTags: false,
- layoutAwareScan: false
- },
-
- translation: {
- convertHalfWidthCharacters: 'false',
- convertNumericCharacters: 'false',
- convertAlphabeticCharacters: 'false',
- convertHiraganaToKatakana: 'false',
- convertKatakanaToHiragana: 'variant',
- collapseEmphaticSequences: 'false'
- },
-
- dictionaries: {},
-
- parsing: {
- enableScanningParser: true,
- enableMecabParser: false,
- selectedParser: null,
- termSpacing: true,
- readingMode: 'hiragana'
- },
-
- anki: {
- enable: false,
- server: 'http://127.0.0.1:8765',
- tags: ['yomichan'],
- sentenceExt: 200,
- screenshot: {format: 'png', quality: 92},
- terms: {deck: '', model: '', fields: {}},
- kanji: {deck: '', model: '', fields: {}},
- duplicateScope: 'collection',
- fieldTemplates: null
- }
- };
- }
-
- _legacyProfileUpdateAssignDefaults(options) {
- const defaults = this._legacyProfileUpdateGetDefaults();
-
- const combine = (target, source) => {
- for (const key in source) {
- if (!Object.prototype.hasOwnProperty.call(target, key)) {
- target[key] = source[key];
- }
- }
- };
-
- combine(options, defaults);
- combine(options.general, defaults.general);
- combine(options.scanning, defaults.scanning);
- combine(options.anki, defaults.anki);
- combine(options.anki.terms, defaults.anki.terms);
- combine(options.anki.kanji, defaults.anki.kanji);
-
- return options;
- }
-
- _legacyProfileUpdateUpdateVersion(options) {
- const updates = this._legacyProfileUpdateGetUpdates();
- this._legacyProfileUpdateAssignDefaults(options);
-
- const targetVersion = updates.length;
- const currentVersion = options.version;
-
- if (typeof currentVersion === 'number' && Number.isFinite(currentVersion)) {
- for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) {
- const update = updates[i];
- if (update !== null) {
- update(options);
- }
- }
- }
-
- options.version = targetVersion;
- return options;
- }
-
- // Private
-
- async _applyAnkiFieldTemplatesPatch(options, modificationsUrl) {
- let patch = null;
- for (const {options: profileOptions} of options.profiles) {
- const fieldTemplates = profileOptions.anki.fieldTemplates;
- if (fieldTemplates === null) { continue; }
-
- if (patch === null) {
- const content = await this._fetchAsset(modificationsUrl);
- if (this._templatePatcher === null) {
- this._templatePatcher = new TemplatePatcher();
- }
- patch = this._templatePatcher.parsePatch(content);
- }
-
- profileOptions.anki.fieldTemplates = this._templatePatcher.applyPatch(fieldTemplates, patch);
- }
- }
-
- async _fetchAsset(url, json=false) {
- url = chrome.runtime.getURL(url);
- const response = await fetch(url, {
- method: 'GET',
- mode: 'no-cors',
- cache: 'default',
- credentials: 'omit',
- redirect: 'follow',
- referrerPolicy: 'no-referrer'
- });
- if (!response.ok) {
- throw new Error(`Failed to fetch ${url}: ${response.status}`);
- }
- return await (json ? response.json() : response.text());
- }
-
- _getStringHashCode(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;
- }
-
- return hashCode;
- }
-
- async _applyUpdates(options, updates) {
- const targetVersion = updates.length;
- let currentVersion = options.version;
-
- if (typeof currentVersion !== 'number' || !Number.isFinite(currentVersion)) {
- currentVersion = 0;
- }
-
- for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) {
- const {update, async} = updates[i];
- const result = update(options);
- options = (async ? await result : result);
- }
-
- options.version = targetVersion;
- return options;
- }
-
- _getVersionUpdates() {
- return [
- {async: false, update: this._updateVersion1.bind(this)},
- {async: false, update: this._updateVersion2.bind(this)},
- {async: true, update: this._updateVersion3.bind(this)},
- {async: true, update: this._updateVersion4.bind(this)},
- {async: false, update: this._updateVersion5.bind(this)},
- {async: true, update: this._updateVersion6.bind(this)},
- {async: false, update: this._updateVersion7.bind(this)},
- {async: true, update: this._updateVersion8.bind(this)}
- ];
- }
-
- _updateVersion1(options) {
- // Version 1 changes:
- // Added options.global.database.prefixWildcardsSupported = false.
- options.global = {
- database: {
- prefixWildcardsSupported: false
- }
- };
- return options;
- }
-
- _updateVersion2(options) {
- // Version 2 changes:
- // Legacy profile update process moved into this upgrade function.
- for (const profile of options.profiles) {
- if (!Array.isArray(profile.conditionGroups)) {
- profile.conditionGroups = [];
- }
- profile.options = this._legacyProfileUpdateUpdateVersion(profile.options);
- }
- return options;
- }
-
- async _updateVersion3(options) {
- // Version 3 changes:
- // Pitch accent Anki field templates added.
- await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v2.handlebars');
- return options;
- }
-
- async _updateVersion4(options) {
- // Version 4 changes:
- // Options conditions converted to string representations.
- // Added usePopupWindow.
- // Updated handlebars templates to include "clipboard-image" definition.
- // Updated handlebars templates to include "clipboard-text" definition.
- // Added hideDelay.
- // Added inputs to profileOptions.scanning.
- // Added pointerEventsEnabled to profileOptions.scanning.
- // Added preventMiddleMouse to profileOptions.scanning.
- for (const {conditionGroups} of options.profiles) {
- for (const {conditions} of conditionGroups) {
- for (const condition of conditions) {
- const value = condition.value;
- condition.value = (
- Array.isArray(value) ?
- value.join(', ') :
- `${value}`
- );
- }
- }
- }
- const createInputDefaultOptions = () => ({
- showAdvanced: false,
- searchTerms: true,
- searchKanji: true,
- scanOnTouchMove: true,
- scanOnPenHover: true,
- scanOnPenPress: true,
- scanOnPenRelease: false,
- preventTouchScrolling: true
- });
- for (const {options: profileOptions} of options.profiles) {
- profileOptions.general.usePopupWindow = false;
- profileOptions.scanning.hideDelay = 0;
- profileOptions.scanning.pointerEventsEnabled = false;
- profileOptions.scanning.preventMiddleMouse = {
- onWebPages: false,
- onPopupPages: false,
- onSearchPages: false,
- onSearchQuery: false
- };
-
- const {modifier, middleMouse} = profileOptions.scanning;
- delete profileOptions.scanning.modifier;
- delete profileOptions.scanning.middleMouse;
- const scanningInputs = [];
- let modifierInput = '';
- switch (modifier) {
- case 'alt':
- case 'ctrl':
- case 'shift':
- case 'meta':
- modifierInput = modifier;
- break;
- case 'none':
- modifierInput = '';
- break;
- }
- scanningInputs.push({
- include: modifierInput,
- exclude: 'mouse0',
- types: {mouse: true, touch: false, pen: false},
- options: createInputDefaultOptions()
- });
- if (middleMouse) {
- scanningInputs.push({
- include: 'mouse2',
- exclude: '',
- types: {mouse: true, touch: false, pen: false},
- options: createInputDefaultOptions()
- });
- }
- scanningInputs.push({
- include: '',
- exclude: '',
- types: {mouse: false, touch: true, pen: true},
- options: createInputDefaultOptions()
- });
- profileOptions.scanning.inputs = scanningInputs;
- }
- await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v4.handlebars');
- return options;
- }
-
- _updateVersion5(options) {
- // Version 5 changes:
- // Removed legacy version number from profile options.
- for (const profile of options.profiles) {
- delete profile.options.version;
- }
- return options;
- }
-
- async _updateVersion6(options) {
- // Version 6 changes:
- // Updated handlebars templates to include "conjugation" definition.
- // Added global option showPopupPreview.
- // Added global option useSettingsV2.
- // Added anki.checkForDuplicates.
- // Added general.glossaryLayoutMode; removed general.compactGlossaries.
- await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v6.handlebars');
- options.global.showPopupPreview = false;
- options.global.useSettingsV2 = false;
- for (const profile of options.profiles) {
- profile.options.anki.checkForDuplicates = true;
- profile.options.general.glossaryLayoutMode = (profile.options.general.compactGlossaries ? 'compact' : 'default');
- delete profile.options.general.compactGlossaries;
- const fieldTemplates = profile.options.anki.fieldTemplates;
- if (typeof fieldTemplates === 'string') {
- profile.options.anki.fieldTemplates = this._updateVersion6AnkiTemplatesCompactTags(fieldTemplates);
- }
- }
- return options;
- }
-
- _updateVersion6AnkiTemplatesCompactTags(templates) {
- const rawPattern1 = '{{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}';
- const pattern1 = new RegExp(`((\r?\n)?[ \t]*)${escapeRegExp(rawPattern1)}`, 'g');
- const replacement1 = (
- // eslint-disable-next-line indent
-`{{~#scope~}}
- {{~#set "any" false}}{{/set~}}
- {{~#if definitionTags~}}{{#each definitionTags~}}
- {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}}
- {{~#if (get "any")}}, {{else}}<i>({{/if~}}
- {{name}}
- {{~#set "any" true}}{{/set~}}
- {{~/if~}}
- {{~/each~}}
- {{~#if (get "any")}})</i> {{/if~}}
- {{~/if~}}
-{{~/scope~}}`
- );
- const simpleNewline = /\n/g;
- templates = templates.replace(pattern1, (g0, space) => (space + replacement1.replace(simpleNewline, space)));
- templates = templates.replace(/\bcompactGlossaries=((?:\.*\/)*)compactGlossaries\b/g, (g0, g1) => `${g0} data=${g1}.`);
- return templates;
- }
-
- _updateVersion7(options) {
- // Version 7 changes:
- // Added general.maximumClipboardSearchLength.
- // Added general.popupCurrentIndicatorMode.
- // Added general.popupActionBarVisibility.
- // Added general.popupActionBarLocation.
- // Removed global option showPopupPreview.
- delete options.global.showPopupPreview;
- for (const profile of options.profiles) {
- profile.options.general.maximumClipboardSearchLength = 1000;
- profile.options.general.popupCurrentIndicatorMode = 'triangle';
- profile.options.general.popupActionBarVisibility = 'auto';
- profile.options.general.popupActionBarLocation = 'right';
- }
- return options;
- }
-
- async _updateVersion8(options) {
- // Version 8 changes:
- // Added translation.textReplacements.
- // Moved anki.sentenceExt to sentenceParsing.scanExtent.
- // Added sentenceParsing.enableTerminationCharacters.
- // Added sentenceParsing.terminationCharacters.
- // Changed general.popupActionBarLocation.
- // Added inputs.hotkeys.
- // Added anki.suspendNewCards.
- // Added popupWindow.
- // Updated handlebars templates to include "stroke-count" definition.
- // Updated global.useSettingsV2 to be true (opt-out).
- // Added audio.customSourceType.
- // Moved general.enableClipboardPopups => clipboard.enableBackgroundMonitor.
- // Moved general.enableClipboardMonitor => clipboard.enableSearchPageMonitor. Forced value to false due to a bug which caused its value to not be read.
- // Moved general.maximumClipboardSearchLength => clipboard.maximumSearchLength.
- // Added clipboard.autoSearchContent.
- await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v8.handlebars');
- options.global.useSettingsV2 = true;
- for (const profile of options.profiles) {
- profile.options.translation.textReplacements = {
- searchOriginal: true,
- groups: []
- };
- profile.options.sentenceParsing = {
- scanExtent: profile.options.anki.sentenceExt,
- enableTerminationCharacters: true,
- terminationCharacters: [
- {enabled: true, character1: '「', character2: '」', includeCharacterAtStart: false, includeCharacterAtEnd: false},
- {enabled: true, character1: '『', character2: '』', includeCharacterAtStart: false, includeCharacterAtEnd: false},
- {enabled: true, character1: '"', character2: '"', includeCharacterAtStart: false, includeCharacterAtEnd: false},
- {enabled: true, character1: '\'', character2: '\'', includeCharacterAtStart: false, includeCharacterAtEnd: false},
- {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
- {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
- {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
- {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
- {enabled: true, character1: '。', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
- {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
- {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true},
- {enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}
- ]
- };
- delete profile.options.anki.sentenceExt;
- profile.options.general.popupActionBarLocation = 'top';
- profile.options.inputs = {
- hotkeys: [
- {action: 'close', key: 'Escape', modifiers: [], scopes: ['popup'], enabled: true},
- {action: 'focusSearchBox', key: 'Escape', modifiers: [], scopes: ['search'], enabled: true},
- {action: 'previousEntry3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'nextEntry3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'lastEntry', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'firstEntry', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'previousEntry', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'nextEntry', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'historyBackward', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'historyForward', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'addNoteKanji', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'addNoteTermKanji', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'addNoteTermKana', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'playAudio', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'viewNote', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true},
- {action: 'copyHostSelection', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup'], enabled: true}
- ]
- };
- profile.options.anki.suspendNewCards = false;
- profile.options.popupWindow = {
- width: profile.options.general.popupWidth,
- height: profile.options.general.popupHeight,
- left: 0,
- top: 0,
- useLeft: false,
- useTop: false,
- windowType: 'popup',
- windowState: 'normal'
- };
- profile.options.audio.customSourceType = 'audio';
- profile.options.clipboard = {
- enableBackgroundMonitor: profile.options.general.enableClipboardPopups,
- enableSearchPageMonitor: false,
- autoSearchContent: true,
- maximumSearchLength: profile.options.general.maximumClipboardSearchLength
- };
- delete profile.options.general.enableClipboardPopups;
- delete profile.options.general.enableClipboardMonitor;
- delete profile.options.general.maximumClipboardSearchLength;
- }
- return options;
- }
-}
diff --git a/ext/bg/js/permissions-main.js b/ext/bg/js/permissions-main.js
deleted file mode 100644
index 5b17a5dd..00000000
--- a/ext/bg/js/permissions-main.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * DocumentFocusController
- * PermissionsToggleController
- * SettingsController
- * api
- */
-
-async function setupEnvironmentInfo() {
- const {manifest_version: manifestVersion} = chrome.runtime.getManifest();
- const {browser, platform} = await api.getEnvironmentInfo();
- document.documentElement.dataset.browser = browser;
- document.documentElement.dataset.os = platform.os;
- document.documentElement.dataset.manifestVersion = `${manifestVersion}`;
-}
-
-async function isAllowedIncognitoAccess() {
- return await new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve));
-}
-
-async function isAllowedFileSchemeAccess() {
- return await new Promise((resolve) => chrome.extension.isAllowedFileSchemeAccess(resolve));
-}
-
-function setupPermissionsToggles() {
- const manifest = chrome.runtime.getManifest();
- let optionalPermissions = manifest.optional_permissions;
- if (!Array.isArray(optionalPermissions)) { optionalPermissions = []; }
- optionalPermissions = new Set(optionalPermissions);
-
- const hasAllPermisions = (set, values) => {
- for (const value of values) {
- if (!set.has(value)) { return false; }
- }
- return true;
- };
-
- for (const toggle of document.querySelectorAll('.permissions-toggle')) {
- let permissions = toggle.dataset.requiredPermissions;
- permissions = (typeof permissions === 'string' && permissions.length > 0 ? permissions.split(' ') : []);
- toggle.disabled = !hasAllPermisions(optionalPermissions, permissions);
- }
-}
-
-(async () => {
- try {
- const documentFocusController = new DocumentFocusController();
- documentFocusController.prepare();
-
- setupPermissionsToggles();
-
- for (const node of document.querySelectorAll('.extension-id-example')) {
- node.textContent = chrome.runtime.getURL('/');
- }
-
- api.forwardLogsToBackend();
- await yomichan.prepare();
-
- setupEnvironmentInfo();
-
- const permissionsCheckboxes = [
- document.querySelector('#permission-checkbox-allow-in-private-windows'),
- document.querySelector('#permission-checkbox-allow-file-url-access')
- ];
-
- const permissions = await Promise.all([
- isAllowedIncognitoAccess(),
- isAllowedFileSchemeAccess()
- ]);
-
- for (let i = 0, ii = permissions.length; i < ii; ++i) {
- permissionsCheckboxes[i].checked = permissions[i];
- }
-
- const settingsController = new SettingsController(0);
- settingsController.prepare();
-
- const permissionsToggleController = new PermissionsToggleController(settingsController);
- permissionsToggleController.prepare();
-
- await promiseTimeout(100);
-
- document.documentElement.dataset.loaded = 'true';
- } catch (e) {
- yomichan.logError(e);
- }
-})();
diff --git a/ext/bg/js/permissions-util.js b/ext/bg/js/permissions-util.js
deleted file mode 100644
index bd3a18ce..00000000
--- a/ext/bg/js/permissions-util.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2021 Yomichan Authors
- *
- * 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 PermissionsUtil {
- constructor() {
- this._ankiFieldMarkersRequiringClipboardPermission = new Set([
- 'clipboard-image',
- 'clipboard-text'
- ]);
- this._ankiMarkerPattern = /\{([\w-]+)\}/g;
- }
-
- hasPermissions(permissions) {
- return new Promise((resolve, reject) => chrome.permissions.contains(permissions, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- }));
- }
-
- setPermissionsGranted(permissions, shouldHave) {
- return (
- shouldHave ?
- new Promise((resolve, reject) => chrome.permissions.request(permissions, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- })) :
- new Promise((resolve, reject) => chrome.permissions.remove(permissions, (result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(!result);
- }
- }))
- );
- }
-
- getAllPermissions() {
- return new Promise((resolve, reject) => chrome.permissions.getAll((result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- }));
- }
-
- getRequiredPermissionsForAnkiFieldValue(fieldValue) {
- const markers = this._getAnkiFieldMarkers(fieldValue);
- const markerPermissions = this._ankiFieldMarkersRequiringClipboardPermission;
- for (const marker of markers) {
- if (markerPermissions.has(marker)) {
- return ['clipboardRead'];
- }
- }
- return [];
- }
-
- hasRequiredPermissionsForOptions(permissions, options) {
- const permissionsSet = new Set(permissions.permissions);
-
- if (!permissionsSet.has('nativeMessaging')) {
- if (options.parsing.enableMecabParser) {
- return false;
- }
- }
-
- if (!permissionsSet.has('clipboardRead')) {
- if (options.clipboard.enableBackgroundMonitor || options.clipboard.enableSearchPageMonitor) {
- return false;
- }
- const fieldMarkersRequiringClipboardPermission = this._ankiFieldMarkersRequiringClipboardPermission;
- const fieldsList = [
- options.anki.terms.fields,
- options.anki.kanji.fields
- ];
- for (const fields of fieldsList) {
- for (const fieldValue of Object.values(fields)) {
- const markers = this._getAnkiFieldMarkers(fieldValue);
- for (const marker of markers) {
- if (fieldMarkersRequiringClipboardPermission.has(marker)) {
- return false;
- }
- }
- }
- }
- }
-
- return true;
- }
-
- // Private
-
- _getAnkiFieldMarkers(fieldValue) {
- const pattern = this._ankiMarkerPattern;
- const markers = [];
- let match;
- while ((match = pattern.exec(fieldValue)) !== null) {
- markers.push(match[1]);
- }
- return markers;
- }
-}
diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js
deleted file mode 100644
index 8e6c7163..00000000
--- a/ext/bg/js/profile-conditions.js
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/**
- * Utility class to help processing profile conditions.
- */
-class ProfileConditions {
- /**
- * Creates a new instance.
- */
- constructor() {
- this._splitPattern = /[,;\s]+/;
- this._descriptors = new Map([
- [
- 'popupLevel',
- {
- operators: new Map([
- ['equal', this._createSchemaPopupLevelEqual.bind(this)],
- ['notEqual', this._createSchemaPopupLevelNotEqual.bind(this)],
- ['lessThan', this._createSchemaPopupLevelLessThan.bind(this)],
- ['greaterThan', this._createSchemaPopupLevelGreaterThan.bind(this)],
- ['lessThanOrEqual', this._createSchemaPopupLevelLessThanOrEqual.bind(this)],
- ['greaterThanOrEqual', this._createSchemaPopupLevelGreaterThanOrEqual.bind(this)]
- ])
- }
- ],
- [
- 'url',
- {
- operators: new Map([
- ['matchDomain', this._createSchemaUrlMatchDomain.bind(this)],
- ['matchRegExp', this._createSchemaUrlMatchRegExp.bind(this)]
- ])
- }
- ],
- [
- 'modifierKeys',
- {
- operators: new Map([
- ['are', this._createSchemaModifierKeysAre.bind(this)],
- ['areNot', this._createSchemaModifierKeysAreNot.bind(this)],
- ['include', this._createSchemaModifierKeysInclude.bind(this)],
- ['notInclude', this._createSchemaModifierKeysNotInclude.bind(this)]
- ])
- }
- ]
- ]);
- }
-
- /**
- * Creates a new JSON schema descriptor for the given set of condition groups.
- * @param conditionGroups An array of condition groups in the following format:
- * conditionGroups = [
- * {
- * conditions: [
- * {
- * type: (condition type: string),
- * operator: (condition sub-type: string),
- * value: (value to compare against: string)
- * },
- * ...
- * ]
- * },
- * ...
- * ]
- */
- createSchema(conditionGroups) {
- const anyOf = [];
- for (const {conditions} of conditionGroups) {
- const allOf = [];
- for (const {type, operator, value} of conditions) {
- const conditionDescriptor = this._descriptors.get(type);
- if (typeof conditionDescriptor === 'undefined') { continue; }
-
- const createSchema = conditionDescriptor.operators.get(operator);
- if (typeof createSchema === 'undefined') { continue; }
-
- const schema = createSchema(value);
- allOf.push(schema);
- }
- switch (allOf.length) {
- case 0: break;
- case 1: anyOf.push(allOf[0]); break;
- default: anyOf.push({allOf}); break;
- }
- }
- switch (anyOf.length) {
- case 0: return {};
- case 1: return anyOf[0];
- default: return {anyOf};
- }
- }
-
- /**
- * Creates a normalized version of the context object to test,
- * assigning dependent fields as needed.
- * @param context A context object which is used during schema validation.
- * @returns A normalized context object.
- */
- normalizeContext(context) {
- const normalizedContext = Object.assign({}, context);
- const {url} = normalizedContext;
- if (typeof url === 'string') {
- try {
- normalizedContext.domain = new URL(url).hostname;
- } catch (e) {
- // NOP
- }
- }
- return normalizedContext;
- }
-
- // Private
-
- _split(value) {
- return value.split(this._splitPattern);
- }
-
- _stringToNumber(value) {
- const number = Number.parseFloat(value);
- return Number.isFinite(number) ? number : 0;
- }
-
- // popupLevel schema creation functions
-
- _createSchemaPopupLevelEqual(value) {
- value = this._stringToNumber(value);
- return {
- required: ['depth'],
- properties: {
- depth: {const: value}
- }
- };
- }
-
- _createSchemaPopupLevelNotEqual(value) {
- return {
- not: [this._createSchemaPopupLevelEqual(value)]
- };
- }
-
- _createSchemaPopupLevelLessThan(value) {
- value = this._stringToNumber(value);
- return {
- required: ['depth'],
- properties: {
- depth: {type: 'number', exclusiveMaximum: value}
- }
- };
- }
-
- _createSchemaPopupLevelGreaterThan(value) {
- value = this._stringToNumber(value);
- return {
- required: ['depth'],
- properties: {
- depth: {type: 'number', exclusiveMinimum: value}
- }
- };
- }
-
- _createSchemaPopupLevelLessThanOrEqual(value) {
- value = this._stringToNumber(value);
- return {
- required: ['depth'],
- properties: {
- depth: {type: 'number', maximum: value}
- }
- };
- }
-
- _createSchemaPopupLevelGreaterThanOrEqual(value) {
- value = this._stringToNumber(value);
- return {
- required: ['depth'],
- properties: {
- depth: {type: 'number', minimum: value}
- }
- };
- }
-
- // url schema creation functions
-
- _createSchemaUrlMatchDomain(value) {
- const oneOf = [];
- for (let domain of this._split(value)) {
- if (domain.length === 0) { continue; }
- domain = domain.toLowerCase();
- oneOf.push({const: domain});
- }
- return {
- required: ['domain'],
- properties: {
- domain: {oneOf}
- }
- };
- }
-
- _createSchemaUrlMatchRegExp(value) {
- return {
- required: ['url'],
- properties: {
- url: {type: 'string', pattern: value, patternFlags: 'i'}
- }
- };
- }
-
- // modifierKeys schema creation functions
-
- _createSchemaModifierKeysAre(value) {
- return this._createSchemaModifierKeysGeneric(value, true, false);
- }
-
- _createSchemaModifierKeysAreNot(value) {
- return {
- not: [this._createSchemaModifierKeysGeneric(value, true, false)]
- };
- }
-
- _createSchemaModifierKeysInclude(value) {
- return this._createSchemaModifierKeysGeneric(value, false, false);
- }
-
- _createSchemaModifierKeysNotInclude(value) {
- return this._createSchemaModifierKeysGeneric(value, false, true);
- }
-
- _createSchemaModifierKeysGeneric(value, exact, none) {
- const containsList = [];
- for (const modifierKey of this._split(value)) {
- if (modifierKey.length === 0) { continue; }
- containsList.push({
- contains: {
- const: modifierKey
- }
- });
- }
- const containsListCount = containsList.length;
- const modifierKeysSchema = {
- type: 'array'
- };
- if (exact) {
- modifierKeysSchema.maxItems = containsListCount;
- }
- if (none) {
- if (containsListCount > 0) {
- modifierKeysSchema.not = containsList;
- }
- } else {
- modifierKeysSchema.minItems = containsListCount;
- if (containsListCount > 0) {
- modifierKeysSchema.allOf = containsList;
- }
- }
- return {
- required: ['modifierKeys'],
- properties: {
- modifierKeys: modifierKeysSchema
- }
- };
- }
-}
diff --git a/ext/bg/js/query-parser.js b/ext/bg/js/query-parser.js
deleted file mode 100644
index 05ebfa27..00000000
--- a/ext/bg/js/query-parser.js
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Copyright (C) 2019-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * TextScanner
- * api
- */
-
-class QueryParser extends EventDispatcher {
- constructor({getSearchContext, documentUtil}) {
- super();
- this._getSearchContext = getSearchContext;
- this._documentUtil = documentUtil;
- this._text = '';
- this._setTextToken = null;
- this._selectedParser = null;
- this._parseResults = [];
- this._queryParser = document.querySelector('#query-parser-content');
- this._queryParserModeContainer = document.querySelector('#query-parser-mode-container');
- this._queryParserModeSelect = document.querySelector('#query-parser-mode-select');
- this._textScanner = new TextScanner({
- node: this._queryParser,
- getSearchContext,
- documentUtil,
- searchTerms: true,
- searchKanji: false,
- searchOnClick: true
- });
- }
-
- get text() {
- return this._text;
- }
-
- prepare() {
- this._textScanner.prepare();
- this._textScanner.on('searched', this._onSearched.bind(this));
- this._queryParserModeSelect.addEventListener('change', this._onParserChange.bind(this), false);
- }
-
- setOptions({selectedParser, termSpacing, scanning}) {
- let selectedParserChanged = false;
- if (selectedParser === null || typeof selectedParser === 'string') {
- selectedParserChanged = (this._selectedParser !== selectedParser);
- this._selectedParser = selectedParser;
- }
- if (typeof termSpacing === 'boolean') {
- this._queryParser.dataset.termSpacing = `${termSpacing}`;
- }
- if (scanning !== null && typeof scanning === 'object') {
- this._textScanner.setOptions(scanning);
- }
- this._textScanner.setEnabled(true);
- if (selectedParserChanged && this._parseResults.length > 0) {
- this._renderParseResult();
- }
- }
-
- async setText(text) {
- this._text = text;
- this._setPreview(text);
-
- const token = {};
- this._setTextToken = token;
- this._parseResults = await api.textParse(text, this._getOptionsContext());
- if (this._setTextToken !== token) { return; }
-
- this._refreshSelectedParser();
-
- this._renderParserSelect();
- this._renderParseResult();
- }
-
- // Private
-
- _onSearched(e) {
- const {error} = e;
- if (error !== null) {
- yomichan.logError(error);
- return;
- }
- if (e.type === null) { return; }
-
- this.trigger('searched', e);
- }
-
- _onParserChange(e) {
- const value = e.currentTarget.value;
- this._setSelectedParser(value);
- }
-
- _getOptionsContext() {
- return this._getSearchContext().optionsContext;
- }
-
- _refreshSelectedParser() {
- if (this._parseResults.length > 0 && !this._getParseResult()) {
- const value = this._parseResults[0].id;
- this._setSelectedParser(value);
- }
- }
-
- _setSelectedParser(value) {
- const optionsContext = this._getOptionsContext();
- api.modifySettings([{
- action: 'set',
- path: 'parsing.selectedParser',
- value,
- scope: 'profile',
- optionsContext
- }], 'search');
- }
-
- _getParseResult() {
- const selectedParser = this._selectedParser;
- return this._parseResults.find((r) => r.id === selectedParser);
- }
-
- _setPreview(text) {
- const terms = [[{text, reading: ''}]];
- this._queryParser.textContent = '';
- this._queryParser.appendChild(this._createParseResult(terms, true));
- }
-
- _renderParserSelect() {
- const visible = (this._parseResults.length > 1);
- if (visible) {
- this._updateParserModeSelect(this._queryParserModeSelect, this._parseResults, this._selectedParser);
- }
- this._queryParserModeContainer.hidden = !visible;
- }
-
- _renderParseResult() {
- const parseResult = this._getParseResult();
- this._queryParser.textContent = '';
- if (!parseResult) { return; }
- this._queryParser.appendChild(this._createParseResult(parseResult.content, false));
- }
-
- _updateParserModeSelect(select, parseResults, selectedParser) {
- const fragment = document.createDocumentFragment();
-
- let index = 0;
- let selectedIndex = -1;
- for (const parseResult of parseResults) {
- const option = document.createElement('option');
- option.value = parseResult.id;
- switch (parseResult.source) {
- case 'scanning-parser':
- option.textContent = 'Scanning parser';
- break;
- case 'mecab':
- option.textContent = `MeCab: ${parseResult.dictionary}`;
- break;
- default:
- option.textContent = `Unknown source: ${parseResult.source}`;
- break;
- }
- fragment.appendChild(option);
-
- if (selectedParser === parseResult.id) {
- selectedIndex = index;
- }
- ++index;
- }
-
- select.textContent = '';
- select.appendChild(fragment);
- select.selectedIndex = selectedIndex;
- }
-
- _createParseResult(terms, preview) {
- const type = preview ? 'preview' : 'normal';
- const fragment = document.createDocumentFragment();
- for (const term of terms) {
- const termNode = document.createElement('span');
- termNode.className = 'query-parser-term';
- termNode.dataset.type = type;
- for (const segment of term) {
- if (segment.reading.trim().length === 0) {
- this._addSegmentText(segment.text, termNode);
- } else {
- termNode.appendChild(this._createSegment(segment));
- }
- }
- fragment.appendChild(termNode);
- }
- return fragment;
- }
-
- _createSegment(segment) {
- const segmentNode = document.createElement('ruby');
- segmentNode.className = 'query-parser-segment';
-
- const textNode = document.createElement('span');
- textNode.className = 'query-parser-segment-text';
-
- const readingNode = document.createElement('rt');
- readingNode.className = 'query-parser-segment-reading';
-
- segmentNode.appendChild(textNode);
- segmentNode.appendChild(readingNode);
-
- this._addSegmentText(segment.text, textNode);
- readingNode.textContent = segment.reading;
-
- return segmentNode;
- }
-
- _addSegmentText(text, container) {
- for (const character of text) {
- const node = document.createElement('span');
- node.className = 'query-parser-char';
- node.textContent = character;
- container.appendChild(node);
- }
- }
-}
diff --git a/ext/bg/js/request-builder.js b/ext/bg/js/request-builder.js
deleted file mode 100644
index dda5825d..00000000
--- a/ext/bg/js/request-builder.js
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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 RequestBuilder {
- constructor() {
- this._extraHeadersSupported = null;
- this._onBeforeSendHeadersExtraInfoSpec = ['blocking', 'requestHeaders', 'extraHeaders'];
- this._textEncoder = new TextEncoder();
- this._ruleIds = new Set();
- }
-
- async prepare() {
- try {
- await this._clearDynamicRules();
- } catch (e) {
- // NOP
- }
- }
-
- async fetchAnonymous(url, init) {
- if (isObject(chrome.declarativeNetRequest)) {
- return await this._fetchAnonymousDeclarative(url, init);
- }
- const originURL = this._getOriginURL(url);
- const modifications = [
- ['cookie', null],
- ['origin', {name: 'Origin', value: originURL}]
- ];
- return await this._fetchModifyHeaders(url, init, modifications);
- }
-
- // Private
-
- async _fetchModifyHeaders(url, init, modifications) {
- const matchURL = this._getMatchURL(url);
-
- let done = false;
- const callback = (details) => {
- if (done || details.url !== url) { return {}; }
- done = true;
-
- const requestHeaders = details.requestHeaders;
- this._modifyHeaders(requestHeaders, modifications);
- return {requestHeaders};
- };
- const filter = {
- urls: [matchURL],
- types: ['xmlhttprequest']
- };
-
- let needsCleanup = false;
- try {
- this._onBeforeSendHeadersAddListener(callback, filter);
- needsCleanup = true;
- } catch (e) {
- // NOP
- }
-
- try {
- return await fetch(url, init);
- } finally {
- if (needsCleanup) {
- try {
- chrome.webRequest.onBeforeSendHeaders.removeListener(callback);
- } catch (e) {
- // NOP
- }
- }
- }
- }
-
- _onBeforeSendHeadersAddListener(callback, filter) {
- const extraInfoSpec = this._onBeforeSendHeadersExtraInfoSpec;
- for (let i = 0; i < 2; ++i) {
- try {
- chrome.webRequest.onBeforeSendHeaders.addListener(callback, filter, extraInfoSpec);
- if (this._extraHeadersSupported === null) {
- this._extraHeadersSupported = true;
- }
- break;
- } catch (e) {
- // Firefox doesn't support the 'extraHeaders' option and will throw the following error:
- // Type error for parameter extraInfoSpec (Error processing 2: Invalid enumeration value "extraHeaders") for webRequest.onBeforeSendHeaders.
- if (this._extraHeadersSupported !== null || !`${e.message}`.includes('extraHeaders')) {
- throw e;
- }
- }
-
- // addListener failed; remove 'extraHeaders' from extraInfoSpec.
- this._extraHeadersSupported = false;
- const index = extraInfoSpec.indexOf('extraHeaders');
- if (index >= 0) { extraInfoSpec.splice(index, 1); }
- }
- }
-
- _getMatchURL(url) {
- const url2 = new URL(url);
- return `${url2.protocol}//${url2.host}${url2.pathname}`;
- }
-
- _getOriginURL(url) {
- const url2 = new URL(url);
- return `${url2.protocol}//${url2.host}`;
- }
-
- _modifyHeaders(headers, modifications) {
- modifications = new Map(modifications);
-
- for (let i = 0, ii = headers.length; i < ii; ++i) {
- const header = headers[i];
- const name = header.name.toLowerCase();
- const modification = modifications.get(name);
- if (typeof modification === 'undefined') { continue; }
-
- modifications.delete(name);
-
- if (modification === null) {
- headers.splice(i, 1);
- --i;
- --ii;
- } else {
- headers[i] = modification;
- }
- }
-
- for (const header of modifications.values()) {
- if (header !== null) {
- headers.push(header);
- }
- }
- }
-
- async _clearDynamicRules() {
- if (!isObject(chrome.declarativeNetRequest)) { return; }
-
- const rules = this._getDynamicRules();
-
- if (rules.length === 0) { return; }
-
- const removeRuleIds = [];
- for (const {id} of rules) {
- removeRuleIds.push(id);
- }
-
- await this._updateDynamicRules({removeRuleIds});
- }
-
- async _fetchAnonymousDeclarative(url, init) {
- const id = this._getNewRuleId();
- const originUrl = this._getOriginURL(url);
- url = encodeURI(decodeURI(url));
-
- this._ruleIds.add(id);
- try {
- const addRules = [{
- id,
- priority: 1,
- condition: {
- urlFilter: `|${this._escapeDnrUrl(url)}|`,
- resourceTypes: ['xmlhttprequest']
- },
- action: {
- type: 'modifyHeaders',
- requestHeaders: [
- {
- operation: 'remove',
- header: 'Cookie'
- },
- {
- operation: 'set',
- header: 'Origin',
- value: originUrl
- }
- ],
- responseHeaders: [
- {
- operation: 'remove',
- header: 'Set-Cookie'
- }
- ]
- }
- }];
-
- await this._updateDynamicRules({addRules});
- try {
- return await fetch(url, init);
- } finally {
- await this._tryUpdateDynamicRules({removeRuleIds: [id]});
- }
- } finally {
- this._ruleIds.delete(id);
- }
- }
-
- _getDynamicRules() {
- return new Promise((resolve, reject) => {
- chrome.declarativeNetRequest.getDynamicRules((result) => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve(result);
- }
- });
- });
- }
-
- _updateDynamicRules(options) {
- return new Promise((resolve, reject) => {
- chrome.declarativeNetRequest.updateDynamicRules(options, () => {
- const e = chrome.runtime.lastError;
- if (e) {
- reject(new Error(e.message));
- } else {
- resolve();
- }
- });
- });
- }
-
- async _tryUpdateDynamicRules(options) {
- try {
- await this._updateDynamicRules(options);
- return true;
- } catch (e) {
- return false;
- }
- }
-
- _getNewRuleId() {
- let id = 1;
- while (this._ruleIds.has(id)) {
- const pre = id;
- ++id;
- if (id === pre) { throw new Error('Could not generate an id'); }
- }
- return id;
- }
-
- _escapeDnrUrl(url) {
- return url.replace(/[|*^]/g, (char) => this._urlEncodeUtf8(char));
- }
-
- _urlEncodeUtf8(text) {
- const array = this._textEncoder.encode(text);
- let result = '';
- for (const byte of array) {
- result += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
- }
- return result;
- }
-}
diff --git a/ext/bg/js/search-display-controller.js b/ext/bg/js/search-display-controller.js
deleted file mode 100644
index a295346d..00000000
--- a/ext/bg/js/search-display-controller.js
+++ /dev/null
@@ -1,422 +0,0 @@
-/*
- * Copyright (C) 2016-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * ClipboardMonitor
- * api
- * wanakana
- */
-
-class SearchDisplayController {
- constructor(tabId, frameId, display, japaneseUtil) {
- this._tabId = tabId;
- this._frameId = frameId;
- this._display = display;
- this._searchButton = document.querySelector('#search-button');
- this._queryInput = document.querySelector('#search-textbox');
- this._introElement = document.querySelector('#intro');
- this._clipboardMonitorEnableCheckbox = document.querySelector('#clipboard-monitor-enable');
- this._wanakanaEnableCheckbox = document.querySelector('#wanakana-enable');
- this._queryInputEvents = new EventListenerCollection();
- this._queryInputEventsSetup = false;
- this._wanakanaEnabled = false;
- this._introVisible = true;
- this._introAnimationTimer = null;
- this._clipboardMonitorEnabled = false;
- this._clipboardMonitor = new ClipboardMonitor({
- japaneseUtil,
- clipboardReader: {
- getText: async () => (await api.clipboardGet())
- }
- });
- this._messageHandlers = new Map();
- this._mode = null;
- }
-
- async prepare() {
- this._updateMode();
-
- await this._display.updateOptions();
-
- chrome.runtime.onMessage.addListener(this._onMessage.bind(this));
- yomichan.on('optionsUpdated', this._onOptionsUpdated.bind(this));
-
- this._display.on('optionsUpdated', this._onDisplayOptionsUpdated.bind(this));
- this._display.on('contentUpdating', this._onContentUpdating.bind(this));
-
- this._display.hotkeyHandler.registerActions([
- ['focusSearchBox', this._onActionFocusSearchBox.bind(this)]
- ]);
- this._registerMessageHandlers([
- ['getMode', {async: false, handler: this._onMessageGetMode.bind(this)}],
- ['setMode', {async: false, handler: this._onMessageSetMode.bind(this)}],
- ['updateSearchQuery', {async: false, handler: this._onExternalSearchUpdate.bind(this)}]
- ]);
-
- this._display.autoPlayAudioDelay = 0;
- this._display.queryParserVisible = true;
- this._display.setHistorySettings({useBrowserHistory: true});
- this._display.setQueryPostProcessor(this._postProcessQuery.bind(this));
-
- this._searchButton.addEventListener('click', this._onSearch.bind(this), false);
- this._wanakanaEnableCheckbox.addEventListener('change', this._onWanakanaEnableChange.bind(this));
- window.addEventListener('copy', this._onCopy.bind(this));
- this._clipboardMonitor.on('change', this._onExternalSearchUpdate.bind(this));
- this._clipboardMonitorEnableCheckbox.addEventListener('change', this._onClipboardMonitorEnableChange.bind(this));
- this._display.hotkeyHandler.on('keydownNonHotkey', this._onKeyDown.bind(this));
-
- this._onDisplayOptionsUpdated({options: this._display.getOptions()});
- }
-
- // Actions
-
- _onActionFocusSearchBox() {
- if (this._queryInput === null) { return; }
- this._queryInput.focus();
- this._queryInput.select();
- }
-
- // Messages
-
- _onMessageSetMode({mode}) {
- this._setMode(mode, true);
- }
-
- _onMessageGetMode() {
- return this._mode;
- }
-
- // Private
-
- _onMessage({action, params}, sender, callback) {
- const messageHandler = this._messageHandlers.get(action);
- if (typeof messageHandler === 'undefined') { return false; }
- return yomichan.invokeMessageHandler(messageHandler, params, callback, sender);
- }
-
- _onKeyDown(e) {
- if (
- document.activeElement !== this._queryInput &&
- !e.ctrlKey &&
- !e.metaKey &&
- !e.altKey &&
- e.key.length === 1
- ) {
- this._queryInput.focus({preventScroll: true});
- }
- }
-
- async _onOptionsUpdated() {
- await this._display.updateOptions();
- const query = this._queryInput.value;
- if (query) {
- this._display.searchLast();
- }
- }
-
- _onDisplayOptionsUpdated({options}) {
- this._clipboardMonitorEnabled = options.clipboard.enableSearchPageMonitor;
- this._updateClipboardMonitorEnabled();
-
- const enableWanakana = !!this._display.getOptions().general.enableWanakana;
- this._wanakanaEnableCheckbox.checked = enableWanakana;
- this._setWanakanaEnabled(enableWanakana);
- }
-
- _onContentUpdating({type, content, source}) {
- let animate = false;
- let valid = false;
- switch (type) {
- case 'terms':
- case 'kanji':
- animate = !!content.animate;
- valid = (typeof source === 'string' && source.length > 0);
- this._display.blurElement(this._queryInput);
- break;
- case 'clear':
- valid = false;
- animate = true;
- source = '';
- break;
- }
-
- if (typeof source !== 'string') { source = ''; }
-
- if (this._queryInput.value !== source) {
- this._queryInput.value = source;
- this._updateSearchHeight(true);
- }
- this._setIntroVisible(!valid, animate);
- }
-
- _onSearchInput() {
- this._updateSearchHeight(false);
- }
-
- _onSearchKeydown(e) {
- const {code} = e;
- if (!((code === 'Enter' || code === 'NumpadEnter') && !e.shiftKey)) { return; }
-
- // Search
- e.preventDefault();
- e.stopImmediatePropagation();
- this._display.blurElement(e.currentTarget);
- this._search(true, true, true);
- }
-
- _onSearch(e) {
- e.preventDefault();
- this._search(true, true, true);
- }
-
- _onCopy() {
- // ignore copy from search page
- this._clipboardMonitor.setPreviousText(window.getSelection().toString().trim());
- }
-
- _onExternalSearchUpdate({text, animate=true}) {
- const {clipboard: {autoSearchContent, maximumSearchLength}} = this._display.getOptions();
- if (text.length > maximumSearchLength) {
- text = text.substring(0, maximumSearchLength);
- }
- this._queryInput.value = text;
- this._updateSearchHeight(true);
- this._search(animate, false, autoSearchContent);
- }
-
- _onWanakanaEnableChange(e) {
- const value = e.target.checked;
- this._setWanakanaEnabled(value);
- api.modifySettings([{
- action: 'set',
- path: 'general.enableWanakana',
- value,
- scope: 'profile',
- optionsContext: this._display.getOptionsContext()
- }], 'search');
- }
-
- _onClipboardMonitorEnableChange(e) {
- const enabled = e.target.checked;
- this._setClipboardMonitorEnabled(enabled);
- }
-
- _setWanakanaEnabled(enabled) {
- if (this._queryInputEventsSetup && this._wanakanaEnabled === enabled) { return; }
-
- const input = this._queryInput;
- this._queryInputEvents.removeAllEventListeners();
- this._queryInputEvents.addEventListener(input, 'keydown', this._onSearchKeydown.bind(this), false);
-
- this._wanakanaEnabled = enabled;
- if (enabled) {
- wanakana.bind(input);
- } else {
- wanakana.unbind(input);
- }
-
- this._queryInputEvents.addEventListener(input, 'input', this._onSearchInput.bind(this), false);
- this._queryInputEventsSetup = true;
- }
-
- _setIntroVisible(visible, animate) {
- if (this._introVisible === visible) {
- return;
- }
-
- this._introVisible = visible;
-
- if (this._introElement === null) {
- return;
- }
-
- if (this._introAnimationTimer !== null) {
- clearTimeout(this._introAnimationTimer);
- this._introAnimationTimer = null;
- }
-
- if (visible) {
- this._showIntro(animate);
- } else {
- this._hideIntro(animate);
- }
- }
-
- _showIntro(animate) {
- if (animate) {
- const duration = 0.4;
- this._introElement.style.transition = '';
- this._introElement.style.height = '';
- const size = this._introElement.getBoundingClientRect();
- this._introElement.style.height = '0px';
- this._introElement.style.transition = `height ${duration}s ease-in-out 0s`;
- window.getComputedStyle(this._introElement).getPropertyValue('height'); // Commits height so next line can start animation
- this._introElement.style.height = `${size.height}px`;
- this._introAnimationTimer = setTimeout(() => {
- this._introElement.style.height = '';
- this._introAnimationTimer = null;
- }, duration * 1000);
- } else {
- this._introElement.style.transition = '';
- this._introElement.style.height = '';
- }
- }
-
- _hideIntro(animate) {
- if (animate) {
- const duration = 0.4;
- const size = this._introElement.getBoundingClientRect();
- this._introElement.style.height = `${size.height}px`;
- this._introElement.style.transition = `height ${duration}s ease-in-out 0s`;
- window.getComputedStyle(this._introElement).getPropertyValue('height'); // Commits height so next line can start animation
- } else {
- this._introElement.style.transition = '';
- }
- this._introElement.style.height = '0';
- }
-
- async _setClipboardMonitorEnabled(value) {
- let modify = true;
- if (value) {
- value = await this._requestPermissions(['clipboardRead']);
- modify = value;
- }
-
- this._clipboardMonitorEnabled = value;
- this._updateClipboardMonitorEnabled();
-
- if (!modify) { return; }
-
- await api.modifySettings([{
- action: 'set',
- path: 'clipboard.enableSearchPageMonitor',
- value,
- scope: 'profile',
- optionsContext: this._display.getOptionsContext()
- }], 'search');
- }
-
- _updateClipboardMonitorEnabled() {
- const enabled = this._clipboardMonitorEnabled;
- this._clipboardMonitorEnableCheckbox.checked = enabled;
- if (enabled && this._mode !== 'popup') {
- this._clipboardMonitor.start();
- } else {
- this._clipboardMonitor.stop();
- }
- }
-
- _requestPermissions(permissions) {
- return new Promise((resolve) => {
- chrome.permissions.request(
- {permissions},
- (granted) => {
- const e = chrome.runtime.lastError;
- resolve(!e && granted);
- }
- );
- });
- }
-
- _search(animate, history, lookup) {
- const query = this._queryInput.value;
- const depth = this._display.depth;
- const url = window.location.href;
- const documentTitle = document.title;
- const details = {
- focus: false,
- history,
- params: {
- query
- },
- state: {
- focusEntry: 0,
- optionsContext: {depth, url},
- url,
- sentence: {text: query, offset: 0},
- documentTitle
- },
- content: {
- definitions: null,
- animate,
- contentOrigin: {
- tabId: this.tabId,
- frameId: this.frameId
- }
- }
- };
- if (!lookup) { details.params.lookup = 'false'; }
- this._display.setContent(details);
- }
-
- _updateSearchHeight(shrink) {
- const node = this._queryInput;
- if (shrink) {
- node.style.height = '0';
- }
- const {scrollHeight} = node;
- const currentHeight = node.getBoundingClientRect().height;
- if (shrink || scrollHeight >= currentHeight - 1) {
- node.style.height = `${scrollHeight}px`;
- }
- }
-
- _postProcessQuery(query) {
- if (this._wanakanaEnabled) {
- try {
- query = this._japaneseUtil.convertToKana(query);
- } catch (e) {
- // NOP
- }
- }
- return query;
- }
-
- _registerMessageHandlers(handlers) {
- for (const [name, handlerInfo] of handlers) {
- this._messageHandlers.set(name, handlerInfo);
- }
- }
-
- _updateMode() {
- let mode = null;
- try {
- mode = sessionStorage.getItem('mode');
- } catch (e) {
- // Browsers can throw a SecurityError when cookie blocking is enabled.
- }
- this._setMode(mode, false);
- }
-
- _setMode(mode, save) {
- if (mode === this._mode) { return; }
- if (save) {
- try {
- if (mode === null) {
- sessionStorage.removeItem('mode');
- } else {
- sessionStorage.setItem('mode', mode);
- }
- } catch (e) {
- // Browsers can throw a SecurityError when cookie blocking is enabled.
- }
- }
- this._mode = mode;
- document.documentElement.dataset.searchMode = (mode !== null ? mode : '');
- this._updateClipboardMonitorEnabled();
- }
-}
diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js
deleted file mode 100644
index c7ec595a..00000000
--- a/ext/bg/js/search-main.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2019-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * Display
- * DocumentFocusController
- * HotkeyHandler
- * JapaneseUtil
- * SearchDisplayController
- * api
- * wanakana
- */
-
-(async () => {
- try {
- const documentFocusController = new DocumentFocusController();
- documentFocusController.prepare();
-
- api.forwardLogsToBackend();
- await yomichan.backendReady();
-
- const {tabId, frameId} = await api.frameInformationGet();
-
- const japaneseUtil = new JapaneseUtil(wanakana);
-
- const hotkeyHandler = new HotkeyHandler();
- hotkeyHandler.prepare();
-
- const display = new Display(tabId, frameId, 'search', japaneseUtil, documentFocusController, hotkeyHandler);
- await display.prepare();
-
- const searchDisplayController = new SearchDisplayController(tabId, frameId, display, japaneseUtil);
- await searchDisplayController.prepare();
-
- display.initializeState();
-
- document.documentElement.dataset.loaded = 'true';
-
- yomichan.ready();
- } catch (e) {
- yomichan.logError(e);
- }
-})();
diff --git a/ext/bg/js/simple-dom-parser.js b/ext/bg/js/simple-dom-parser.js
deleted file mode 100644
index 7c57ca98..00000000
--- a/ext/bg/js/simple-dom-parser.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* globals
- * parse5
- */
-
-class SimpleDOMParser {
- constructor(content) {
- this._document = parse5.parse(content);
- }
-
- getElementById(id, root=null) {
- for (const node of this._allNodes(root)) {
- if (typeof node.tagName === 'string' && this.getAttribute(node, 'id') === id) {
- return node;
- }
- }
- return null;
- }
-
- getElementByTagName(tagName, root=null) {
- for (const node of this._allNodes(root)) {
- if (node.tagName === tagName) {
- return node;
- }
- }
- return null;
- }
-
- getElementsByTagName(tagName, root=null) {
- const results = [];
- for (const node of this._allNodes(root)) {
- if (node.tagName === tagName) {
- results.push(node);
- }
- }
- return results;
- }
-
- getElementsByClassName(className, root=null) {
- const results = [];
- const classNamePattern = new RegExp(`(^|\\s)${escapeRegExp(className)}(\\s|$)`);
- for (const node of this._allNodes(root)) {
- if (typeof node.tagName === 'string') {
- const nodeClassName = this.getAttribute(node, 'class');
- if (nodeClassName !== null && classNamePattern.test(nodeClassName)) {
- results.push(node);
- }
- }
- }
- return results;
- }
-
- getAttribute(element, attribute) {
- for (const attr of element.attrs) {
- if (
- attr.name === attribute &&
- typeof attr.namespace === 'undefined'
- ) {
- return attr.value;
- }
- }
- return null;
- }
-
- getTextContent(element) {
- let source = '';
- for (const node of this._allNodes(element)) {
- if (node.nodeName === '#text') {
- source += node.value;
- }
- }
- return source;
- }
-
- static isSupported() {
- return typeof parse5 !== 'undefined';
- }
-
- // Private
-
- *_allNodes(root) {
- if (root === null) {
- root = this._document;
- }
-
- // Depth-first pre-order traversal
- const nodeQueue = [root];
- while (nodeQueue.length > 0) {
- const node = nodeQueue.pop();
-
- yield node;
-
- const childNodes = node.childNodes;
- if (typeof childNodes !== 'undefined') {
- for (let i = childNodes.length - 1; i >= 0; --i) {
- nodeQueue.push(childNodes[i]);
- }
- }
- }
- }
-}
diff --git a/ext/bg/js/template-patcher.js b/ext/bg/js/template-patcher.js
deleted file mode 100644
index 57178957..00000000
--- a/ext/bg/js/template-patcher.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2021 Yomichan Authors
- *
- * 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 TemplatePatcher {
- constructor() {
- this._diffPattern1 = /\n?\{\{<<<<<<<\}\}\n/g;
- this._diffPattern2 = /\n\{\{=======\}\}\n/g;
- this._diffPattern3 = /\n\{\{>>>>>>>\}\}\n*/g;
- this._lookupMarkerPattern = /[ \t]*\{\{~?>\s*\(\s*lookup\s*\.\s*"marker"\s*\)\s*~?\}\}/g;
- }
-
- parsePatch(content) {
- const diffPattern1 = this._diffPattern1;
- const diffPattern2 = this._diffPattern2;
- const diffPattern3 = this._diffPattern3;
- const modifications = [];
- let index = 0;
-
- while (true) {
- // Find modification boundaries
- diffPattern1.lastIndex = index;
- const m1 = diffPattern1.exec(content);
- if (m1 === null) { break; }
-
- diffPattern2.lastIndex = m1.index + m1[0].length;
- const m2 = diffPattern2.exec(content);
- if (m2 === null) { break; }
-
- diffPattern3.lastIndex = m2.index + m2[0].length;
- const m3 = diffPattern3.exec(content);
- if (m3 === null) { break; }
-
- // Construct
- const current = content.substring(m1.index + m1[0].length, m2.index);
- const replacement = content.substring(m2.index + m2[0].length, m3.index);
-
- if (current.length > 0) {
- modifications.push({current, replacement});
- }
-
- // Update
- content = content.substring(0, m1.index) + content.substring(m3.index + m3[0].length);
- index = m1.index;
- }
-
- return {addition: content, modifications};
- }
-
- applyPatch(template, patch) {
- for (const {current, replacement} of patch.modifications) {
- let fromIndex = 0;
- while (true) {
- const index = template.indexOf(current, fromIndex);
- if (index < 0) { break; }
- template = template.substring(0, index) + replacement + template.substring(index + current.length);
- fromIndex = index + replacement.length;
- }
- }
- template = this._addFieldTemplatesBeforeEnd(template, patch.addition);
- return template;
- }
-
- // Private
-
- _addFieldTemplatesBeforeEnd(template, addition) {
- const newline = '\n';
- let replaced = false;
- template = template.replace(this._lookupMarkerPattern, (g0) => {
- replaced = true;
- return `${addition}${newline}${g0}`;
- });
- if (!replaced) {
- template += newline;
- template += addition;
- }
- return template;
- }
-}
diff --git a/ext/bg/js/template-renderer-frame-api.js b/ext/bg/js/template-renderer-frame-api.js
deleted file mode 100644
index 4936a2af..00000000
--- a/ext/bg/js/template-renderer-frame-api.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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 TemplateRendererFrameApi {
- constructor(templateRenderer) {
- this._templateRenderer = templateRenderer;
- this._windowMessageHandlers = new Map([
- ['render', {async: true, handler: this._onRender.bind(this)}]
- ]);
- }
-
- prepare() {
- window.addEventListener('message', this._onWindowMessage.bind(this), false);
- }
-
- // Private
-
- _onWindowMessage(e) {
- const {source, data: {action, params, id}} = e;
- const messageHandler = this._windowMessageHandlers.get(action);
- if (typeof messageHandler === 'undefined') { return; }
-
- this._onWindowMessageInner(messageHandler, action, params, source, id);
- }
-
- async _onWindowMessageInner({handler, async}, action, params, source, id) {
- let response;
- try {
- let result = handler(params);
- if (async) {
- result = await result;
- }
- response = {result};
- } catch (error) {
- response = {error: this._errorToJson(error)};
- }
-
- if (typeof id === 'undefined') { return; }
- source.postMessage({action: `${action}.response`, params: response, id}, '*');
- }
-
- async _onRender({template, data, type}) {
- return await this._templateRenderer.render(template, data, type);
- }
-
- _errorToJson(error) {
- try {
- if (error !== null && typeof error === 'object') {
- return {
- name: error.name,
- message: error.message,
- stack: error.stack,
- data: error.data
- };
- }
- } catch (e) {
- // NOP
- }
- return {
- value: error,
- hasValue: true
- };
- }
-}
diff --git a/ext/bg/js/template-renderer-frame-main.js b/ext/bg/js/template-renderer-frame-main.js
deleted file mode 100644
index d25eb56d..00000000
--- a/ext/bg/js/template-renderer-frame-main.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* globals
- * AnkiNoteData
- * JapaneseUtil
- * TemplateRenderer
- * TemplateRendererFrameApi
- */
-
-(() => {
- const japaneseUtil = new JapaneseUtil(null);
- const templateRenderer = new TemplateRenderer(japaneseUtil);
- templateRenderer.registerDataType('ankiNote', {
- modifier: ({data, marker}) => new AnkiNoteData(data, marker).createPublic()
- });
- const api = new TemplateRendererFrameApi(templateRenderer);
- api.prepare();
-})();
diff --git a/ext/bg/js/template-renderer-proxy.js b/ext/bg/js/template-renderer-proxy.js
deleted file mode 100644
index 6a49832b..00000000
--- a/ext/bg/js/template-renderer-proxy.js
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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 TemplateRendererProxy {
- constructor() {
- this._frame = null;
- this._frameNeedsLoad = true;
- this._frameLoading = false;
- this._frameLoadPromise = null;
- this._frameUrl = chrome.runtime.getURL('/template-renderer.html');
- this._invocations = new Set();
- }
-
- async render(template, data, type) {
- await this._prepareFrame();
- return await this._invoke('render', {template, data, type});
- }
-
- // Private
-
- async _prepareFrame() {
- if (this._frame === null) {
- this._frame = document.createElement('iframe');
- this._frame.addEventListener('load', this._onFrameLoad.bind(this), false);
- const style = this._frame.style;
- style.opacity = '0';
- style.width = '0';
- style.height = '0';
- style.position = 'absolute';
- style.border = '0';
- style.margin = '0';
- style.padding = '0';
- style.pointerEvents = 'none';
- }
- if (this._frameNeedsLoad) {
- this._frameNeedsLoad = false;
- this._frameLoading = true;
- this._frameLoadPromise = this._loadFrame(this._frame, this._frameUrl)
- .finally(() => { this._frameLoading = false; });
- }
- await this._frameLoadPromise;
- }
-
- _loadFrame(frame, url, timeout=5000) {
- return new Promise((resolve, reject) => {
- let ready = false;
- const cleanup = () => {
- frame.removeEventListener('load', onLoad, false);
- if (timer !== null) {
- clearTimeout(timer);
- timer = null;
- }
- };
- const onLoad = () => {
- if (!ready) { return; }
- cleanup();
- resolve();
- };
-
- let timer = setTimeout(() => {
- cleanup();
- reject(new Error('Timeout'));
- }, timeout);
-
- frame.removeAttribute('src');
- frame.removeAttribute('srcdoc');
- frame.addEventListener('load', onLoad, false);
- try {
- document.body.appendChild(frame);
- ready = true;
- frame.contentDocument.location.href = url;
- } catch (e) {
- cleanup();
- reject(e);
- }
- });
- }
-
- _invoke(action, params, timeout=null) {
- return new Promise((resolve, reject) => {
- const frameWindow = (this._frame !== null ? this._frame.contentWindow : null);
- if (frameWindow === null) {
- reject(new Error('Frame not set up'));
- return;
- }
-
- const id = generateId(16);
- const invocation = {
- cancel: () => {
- cleanup();
- reject(new Error('Terminated'));
- }
- };
-
- const cleanup = () => {
- this._invocations.delete(invocation);
- window.removeEventListener('message', onMessage, false);
- if (timer !== null) {
- clearTimeout(timer);
- timer = null;
- }
- };
- const onMessage = (e) => {
- if (
- e.source !== frameWindow ||
- e.data.id !== id ||
- e.data.action !== `${action}.response`
- ) {
- return;
- }
-
- const response = e.data.params;
- cleanup();
- const {error} = response;
- if (error) {
- reject(deserializeError(error));
- } else {
- resolve(response.result);
- }
- };
-
- let timer = (typeof timeout === 'number' ? setTimeout(() => {
- cleanup();
- reject(new Error('Timeout'));
- }, timeout) : null);
-
- this._invocations.add(invocation);
-
- window.addEventListener('message', onMessage, false);
- frameWindow.postMessage({action, params, id}, '*');
- });
- }
-
- _onFrameLoad() {
- if (this._frameLoading) { return; }
- this._frameNeedsLoad = true;
-
- for (const invocation of this._invocations) {
- invocation.cancel();
- }
- this._invocations.clear();
- }
-}
diff --git a/ext/bg/js/template-renderer.js b/ext/bg/js/template-renderer.js
deleted file mode 100644
index ae39e478..00000000
--- a/ext/bg/js/template-renderer.js
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * Copyright (C) 2016-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * Handlebars
- */
-
-class TemplateRenderer {
- constructor(japaneseUtil) {
- this._japaneseUtil = japaneseUtil;
- this._cache = new Map();
- this._cacheMaxSize = 5;
- this._helpersRegistered = false;
- this._stateStack = null;
- this._dataTypes = new Map();
- }
-
- registerDataType(name, {modifier=null, modifierPost=null}) {
- this._dataTypes.set(name, {modifier, modifierPost});
- }
-
- async render(template, data, type) {
- if (!this._helpersRegistered) {
- this._registerHelpers();
- this._helpersRegistered = true;
- }
-
- const cache = this._cache;
- let instance = cache.get(template);
- if (typeof instance === 'undefined') {
- this._updateCacheSize(this._cacheMaxSize - 1);
- instance = Handlebars.compile(template);
- cache.set(template, instance);
- }
-
- let modifier = null;
- let modifierPost = null;
- if (typeof type === 'string') {
- const typeInfo = this._dataTypes.get(type);
- if (typeof typeInfo !== 'undefined') {
- ({modifier, modifierPost} = typeInfo);
- }
- }
-
- try {
- if (typeof modifier === 'function') {
- data = modifier(data);
- }
-
- this._stateStack = [new Map()];
- return instance(data).trim();
- } finally {
- this._stateStack = null;
-
- if (typeof modifierPost === 'function') {
- modifierPost(data);
- }
- }
- }
-
- // Private
-
- _updateCacheSize(maxSize) {
- const cache = this._cache;
- let removeCount = cache.size - maxSize;
- if (removeCount <= 0) { return; }
-
- for (const key of cache.keys()) {
- cache.delete(key);
- if (--removeCount <= 0) { break; }
- }
- }
-
- _registerHelpers() {
- Handlebars.partials = Handlebars.templates;
-
- const helpers = [
- ['dumpObject', this._dumpObject.bind(this)],
- ['furigana', this._furigana.bind(this)],
- ['furiganaPlain', this._furiganaPlain.bind(this)],
- ['kanjiLinks', this._kanjiLinks.bind(this)],
- ['multiLine', this._multiLine.bind(this)],
- ['sanitizeCssClass', this._sanitizeCssClass.bind(this)],
- ['regexReplace', this._regexReplace.bind(this)],
- ['regexMatch', this._regexMatch.bind(this)],
- ['mergeTags', this._mergeTags.bind(this)],
- ['eachUpTo', this._eachUpTo.bind(this)],
- ['spread', this._spread.bind(this)],
- ['op', this._op.bind(this)],
- ['get', this._get.bind(this)],
- ['set', this._set.bind(this)],
- ['scope', this._scope.bind(this)],
- ['property', this._property.bind(this)],
- ['noop', this._noop.bind(this)],
- ['isMoraPitchHigh', this._isMoraPitchHigh.bind(this)],
- ['getKanaMorae', this._getKanaMorae.bind(this)],
- ['typeof', this._getTypeof.bind(this)]
- ];
-
- for (const [name, helper] of helpers) {
- this._registerHelper(name, helper);
- }
- }
-
- _registerHelper(name, helper) {
- function wrapper(...args) {
- return helper(this, ...args);
- }
- Handlebars.registerHelper(name, wrapper);
- }
-
- _escape(text) {
- return Handlebars.Utils.escapeExpression(text);
- }
-
- _dumpObject(context, options) {
- const dump = JSON.stringify(options.fn(context), null, 4);
- return this._escape(dump);
- }
-
- _furigana(context, ...args) {
- const {expression, reading} = this._getFuriganaExpressionAndReading(context, ...args);
- const segs = this._japaneseUtil.distributeFurigana(expression, reading);
-
- let result = '';
- for (const seg of segs) {
- if (seg.furigana.length > 0) {
- result += `<ruby>${seg.text}<rt>${seg.furigana}</rt></ruby>`;
- } else {
- result += seg.text;
- }
- }
-
- return result;
- }
-
- _furiganaPlain(context, ...args) {
- const {expression, reading} = this._getFuriganaExpressionAndReading(context, ...args);
- const segs = this._japaneseUtil.distributeFurigana(expression, reading);
-
- let result = '';
- for (const seg of segs) {
- if (seg.furigana.length > 0) {
- if (result.length > 0) { result += ' '; }
- result += `${seg.text}[${seg.furigana}]`;
- } else {
- result += seg.text;
- }
- }
-
- return result;
- }
-
- _getFuriganaExpressionAndReading(context, ...args) {
- const options = args[args.length - 1];
- if (args.length >= 3) {
- return {expression: args[0], reading: args[1]};
- } else {
- const {expression, reading} = options.fn(context);
- return {expression, reading};
- }
- }
-
- _kanjiLinks(context, options) {
- const jp = this._japaneseUtil;
- let result = '';
- for (const c of options.fn(context)) {
- if (jp.isCodePointKanji(c.codePointAt(0))) {
- result += `<a href="#" class="kanji-link">${c}</a>`;
- } else {
- result += c;
- }
- }
-
- return result;
- }
-
- _multiLine(context, options) {
- return options.fn(context).split('\n').join('<br>');
- }
-
- _sanitizeCssClass(context, options) {
- return options.fn(context).replace(/[^_a-z0-9\u00a0-\uffff]/ig, '_');
- }
-
- _regexReplace(context, ...args) {
- // Usage:
- // {{#regexReplace regex string [flags]}}content{{/regexReplace}}
- // regex: regular expression string
- // string: string to replace
- // flags: optional flags for regular expression
- // e.g. "i" for case-insensitive, "g" for replace all
- let value = args[args.length - 1].fn(context);
- if (args.length >= 3) {
- try {
- const flags = args.length > 3 ? args[2] : 'g';
- const regex = new RegExp(args[0], flags);
- value = value.replace(regex, args[1]);
- } catch (e) {
- return `${e}`;
- }
- }
- return value;
- }
-
- _regexMatch(context, ...args) {
- // Usage:
- // {{#regexMatch regex [flags]}}content{{/regexMatch}}
- // regex: regular expression string
- // flags: optional flags for regular expression
- // e.g. "i" for case-insensitive, "g" for match all
- let value = args[args.length - 1].fn(context);
- if (args.length >= 2) {
- try {
- const flags = args.length > 2 ? args[1] : '';
- const regex = new RegExp(args[0], flags);
- const parts = [];
- value.replace(regex, (g0) => parts.push(g0));
- value = parts.join('');
- } catch (e) {
- return `${e}`;
- }
- }
- return value;
- }
-
- _mergeTags(context, object, isGroupMode, isMergeMode) {
- const tagSources = [];
- if (isGroupMode || isMergeMode) {
- for (const definition of object.definitions) {
- tagSources.push(definition.definitionTags);
- }
- } else {
- tagSources.push(object.definitionTags);
- }
-
- const tags = new Set();
- for (const tagSource of tagSources) {
- for (const tag of tagSource) {
- tags.add(tag.name);
- }
- }
-
- return [...tags].join(', ');
- }
-
- _eachUpTo(context, iterable, maxCount, options) {
- if (iterable) {
- const results = [];
- let any = false;
- for (const entry of iterable) {
- any = true;
- if (results.length >= maxCount) { break; }
- const processedEntry = options.fn(entry);
- results.push(processedEntry);
- }
- if (any) {
- return results.join('');
- }
- }
- return options.inverse(context);
- }
-
- _spread(context, ...args) {
- const result = [];
- for (let i = 0, ii = args.length - 1; i < ii; ++i) {
- try {
- result.push(...args[i]);
- } catch (e) {
- // NOP
- }
- }
- return result;
- }
-
- _op(context, ...args) {
- switch (args.length) {
- case 3: return this._evaluateUnaryExpression(args[0], args[1]);
- case 4: return this._evaluateBinaryExpression(args[0], args[1], args[2]);
- case 5: return this._evaluateTernaryExpression(args[0], args[1], args[2], args[3]);
- default: return void 0;
- }
- }
-
- _evaluateUnaryExpression(operator, operand1) {
- switch (operator) {
- case '+': return +operand1;
- case '-': return -operand1;
- case '~': return ~operand1;
- case '!': return !operand1;
- default: return void 0;
- }
- }
-
- _evaluateBinaryExpression(operator, operand1, operand2) {
- switch (operator) {
- case '+': return operand1 + operand2;
- case '-': return operand1 - operand2;
- case '/': return operand1 / operand2;
- case '*': return operand1 * operand2;
- case '%': return operand1 % operand2;
- case '**': return operand1 ** operand2;
- case '==': return operand1 == operand2; // eslint-disable-line eqeqeq
- case '!=': return operand1 != operand2; // eslint-disable-line eqeqeq
- case '===': return operand1 === operand2;
- case '!==': return operand1 !== operand2;
- case '<': return operand1 < operand2;
- case '<=': return operand1 <= operand2;
- case '>': return operand1 > operand2;
- case '>=': return operand1 >= operand2;
- case '<<': return operand1 << operand2;
- case '>>': return operand1 >> operand2;
- case '>>>': return operand1 >>> operand2;
- case '&': return operand1 & operand2;
- case '|': return operand1 | operand2;
- case '^': return operand1 ^ operand2;
- case '&&': return operand1 && operand2;
- case '||': return operand1 || operand2;
- default: return void 0;
- }
- }
-
- _evaluateTernaryExpression(operator, operand1, operand2, operand3) {
- switch (operator) {
- case '?:': return operand1 ? operand2 : operand3;
- default: return void 0;
- }
- }
-
- _get(context, key) {
- for (let i = this._stateStack.length; --i >= 0;) {
- const map = this._stateStack[i];
- if (map.has(key)) {
- return map.get(key);
- }
- }
- return void 0;
- }
-
- _set(context, ...args) {
- switch (args.length) {
- case 2:
- {
- const [key, options] = args;
- const value = options.fn(context);
- this._stateStack[this._stateStack.length - 1].set(key, value);
- }
- break;
- case 3:
- {
- const [key, value] = args;
- this._stateStack[this._stateStack.length - 1].set(key, value);
- }
- break;
- }
- return '';
- }
-
- _scope(context, options) {
- try {
- this._stateStack.push(new Map());
- return options.fn(context);
- } finally {
- if (this._stateStack.length > 1) {
- this._stateStack.pop();
- }
- }
- }
-
- _property(context, ...args) {
- const ii = args.length - 1;
- if (ii <= 0) { return void 0; }
-
- try {
- let value = args[0];
- for (let i = 1; i < ii; ++i) {
- value = value[args[i]];
- }
- return value;
- } catch (e) {
- return void 0;
- }
- }
-
- _noop(context, options) {
- return options.fn(context);
- }
-
- _isMoraPitchHigh(context, index, position) {
- return this._japaneseUtil.isMoraPitchHigh(index, position);
- }
-
- _getKanaMorae(context, text) {
- return this._japaneseUtil.getKanaMorae(`${text}`);
- }
-
- _getTypeof(context, ...args) {
- const ii = args.length - 1;
- const value = (ii > 0 ? args[0] : args[ii].fn(context));
- return typeof value;
- }
-}
diff --git a/ext/bg/js/text-source-map.js b/ext/bg/js/text-source-map.js
deleted file mode 100644
index 49b6d99f..00000000
--- a/ext/bg/js/text-source-map.js
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright (C) 2020-2021 Yomichan Authors
- *
- * 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 TextSourceMap {
- constructor(source, mapping=null) {
- this._source = source;
- this._mapping = (mapping !== null ? TextSourceMap.normalizeMapping(mapping) : null);
- }
-
- get source() {
- return this._source;
- }
-
- equals(other) {
- if (this === other) {
- return true;
- }
-
- const source = this._source;
- if (!(other instanceof TextSourceMap && source === other.source)) {
- return false;
- }
-
- let mapping = this._mapping;
- let otherMapping = other.getMappingCopy();
- if (mapping === null) {
- if (otherMapping === null) {
- return true;
- }
- mapping = TextSourceMap.createMapping(source);
- } else if (otherMapping === null) {
- otherMapping = TextSourceMap.createMapping(source);
- }
-
- const mappingLength = mapping.length;
- if (mappingLength !== otherMapping.length) {
- return false;
- }
-
- for (let i = 0; i < mappingLength; ++i) {
- if (mapping[i] !== otherMapping[i]) {
- return false;
- }
- }
-
- return true;
- }
-
- getSourceLength(finalLength) {
- const mapping = this._mapping;
- if (mapping === null) {
- return finalLength;
- }
-
- let sourceLength = 0;
- for (let i = 0; i < finalLength; ++i) {
- sourceLength += mapping[i];
- }
- return sourceLength;
- }
-
- combine(index, count) {
- if (count <= 0) { return; }
-
- if (this._mapping === null) {
- this._mapping = TextSourceMap.createMapping(this._source);
- }
-
- let sum = this._mapping[index];
- const parts = this._mapping.splice(index + 1, count);
- for (const part of parts) {
- sum += part;
- }
- this._mapping[index] = sum;
- }
-
- insert(index, ...items) {
- if (this._mapping === null) {
- this._mapping = TextSourceMap.createMapping(this._source);
- }
-
- this._mapping.splice(index, 0, ...items);
- }
-
- getMappingCopy() {
- return this._mapping !== null ? [...this._mapping] : null;
- }
-
- static createMapping(text) {
- return new Array(text.length).fill(1);
- }
-
- static normalizeMapping(mapping) {
- const result = [];
- for (const value of mapping) {
- result.push(
- (typeof value === 'number' && Number.isFinite(value)) ?
- Math.floor(value) :
- 0
- );
- }
- return result;
- }
-}
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
deleted file mode 100644
index 729c8294..00000000
--- a/ext/bg/js/translator.js
+++ /dev/null
@@ -1,1397 +0,0 @@
-/*
- * Copyright (C) 2016-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * Deinflector
- * TextSourceMap
- */
-
-/**
- * Class which finds term and kanji definitions for text.
- */
-class Translator {
- /**
- * Creates a new Translator instance.
- * @param database An instance of DictionaryDatabase.
- */
- constructor({japaneseUtil, database}) {
- this._japaneseUtil = japaneseUtil;
- this._database = database;
- this._deinflector = null;
- this._tagCache = new Map();
- this._stringComparer = new Intl.Collator('en-US'); // Invariant locale
- }
-
- /**
- * Initializes the instance for use. The public API should not be used until
- * this function has been called.
- * @param deinflectionReasons The raw deinflections reasons data that the Deinflector uses.
- */
- prepare(deinflectionReasons) {
- this._deinflector = new Deinflector(deinflectionReasons);
- }
-
- /**
- * Clears the database tag cache. This should be executed if the database is changed.
- */
- clearDatabaseCaches() {
- this._tagCache.clear();
- }
-
- /**
- * Finds term definitions for the given text.
- * @param mode The mode to use for finding terms, which determines the format of the resulting array.
- * One of: 'group', 'merge', 'split', 'simple'
- * @param text The text to find terms for.
- * @param options An object using the following structure:
- * {
- * wildcard: (enum: null, 'prefix', 'suffix'),
- * mainDictionary: (string),
- * alphanumeric: (boolean),
- * convertHalfWidthCharacters: (enum: 'false', 'true', 'variant'),
- * convertNumericCharacters: (enum: 'false', 'true', 'variant'),
- * convertAlphabeticCharacters: (enum: 'false', 'true', 'variant'),
- * convertHiraganaToKatakana: (enum: 'false', 'true', 'variant'),
- * convertKatakanaToHiragana: (enum: 'false', 'true', 'variant'),
- * collapseEmphaticSequences: (enum: 'false', 'true', 'full'),
- * textReplacements: [
- * (null or [
- * {pattern: (RegExp), replacement: (string)}
- * ...
- * ])
- * ...
- * ],
- * enabledDictionaryMap: (Map of [
- * (string),
- * {
- * priority: (number),
- * allowSecondarySearches: (boolean)
- * }
- * ])
- * }
- * @returns An array of [definitions, textLength]. The structure of each definition depends on the
- * mode parameter, see the _create?TermDefinition?() functions for structure details.
- */
- async findTerms(mode, text, options) {
- switch (mode) {
- case 'group':
- return await this._findTermsGrouped(text, options);
- case 'merge':
- return await this._findTermsMerged(text, options);
- case 'split':
- return await this._findTermsSplit(text, options);
- case 'simple':
- return await this._findTermsSimple(text, options);
- default:
- return [[], 0];
- }
- }
-
- /**
- * Finds kanji definitions for the given text.
- * @param text The text to find kanji definitions for. This string can be of any length,
- * but is typically just one character, which is a single kanji. If the string is multiple
- * characters long, each character will be searched in the database.
- * @param options An object using the following structure:
- * {
- * enabledDictionaryMap: (Map of [
- * (string),
- * {
- * priority: (number)
- * }
- * ])
- * }
- * @returns An array of definitions. See the _createKanjiDefinition() function for structure details.
- */
- async findKanji(text, options) {
- const {enabledDictionaryMap} = options;
- const kanjiUnique = new Set();
- for (const c of text) {
- kanjiUnique.add(c);
- }
-
- const databaseDefinitions = await this._database.findKanjiBulk([...kanjiUnique], enabledDictionaryMap);
- if (databaseDefinitions.length === 0) { return []; }
-
- this._sortDatabaseDefinitionsByIndex(databaseDefinitions);
-
- const definitions = [];
- for (const {character, onyomi, kunyomi, tags, glossary, stats, dictionary} of databaseDefinitions) {
- const expandedStats = await this._expandStats(stats, dictionary);
- const expandedTags = await this._expandTags(tags, dictionary);
- this._sortTags(expandedTags);
-
- const definition = this._createKanjiDefinition(character, dictionary, onyomi, kunyomi, glossary, expandedTags, expandedStats);
- definitions.push(definition);
- }
-
- await this._buildKanjiMeta(definitions, enabledDictionaryMap);
-
- return definitions;
- }
-
- // Find terms core functions
-
- async _findTermsSimple(text, options) {
- const {enabledDictionaryMap} = options;
- const [definitions, length] = await this._findTermsInternal(text, enabledDictionaryMap, options);
- this._sortDefinitions(definitions, false);
- return [definitions, length];
- }
-
- async _findTermsSplit(text, options) {
- const {enabledDictionaryMap} = options;
- const [definitions, length] = await this._findTermsInternal(text, enabledDictionaryMap, options);
- await this._buildTermMeta(definitions, enabledDictionaryMap);
- this._sortDefinitions(definitions, true);
- return [definitions, length];
- }
-
- async _findTermsGrouped(text, options) {
- const {enabledDictionaryMap} = options;
- const [definitions, length] = await this._findTermsInternal(text, enabledDictionaryMap, options);
-
- const groupedDefinitions = this._groupTerms(definitions, enabledDictionaryMap);
- await this._buildTermMeta(groupedDefinitions, enabledDictionaryMap);
- this._sortDefinitions(groupedDefinitions, false);
-
- for (const definition of groupedDefinitions) {
- this._flagRedundantDefinitionTags(definition.definitions);
- }
-
- return [groupedDefinitions, length];
- }
-
- async _findTermsMerged(text, options) {
- const {mainDictionary, enabledDictionaryMap} = options;
- const secondarySearchDictionaryMap = this._getSecondarySearchDictionaryMap(enabledDictionaryMap);
-
- const [definitions, length] = await this._findTermsInternal(text, enabledDictionaryMap, options);
- const {sequencedDefinitions, unsequencedDefinitions} = await this._getSequencedDefinitions(definitions, mainDictionary, enabledDictionaryMap);
- const definitionsMerged = [];
- const usedDefinitions = new Set();
-
- for (const {sourceDefinitions, relatedDefinitions} of sequencedDefinitions) {
- const result = await this._getMergedDefinition(
- sourceDefinitions,
- relatedDefinitions,
- unsequencedDefinitions,
- secondarySearchDictionaryMap,
- usedDefinitions
- );
- definitionsMerged.push(result);
- }
-
- const unusedDefinitions = unsequencedDefinitions.filter((definition) => !usedDefinitions.has(definition));
- for (const groupedDefinition of this._groupTerms(unusedDefinitions, enabledDictionaryMap)) {
- const {reasons, score, expression, reading, source, rawSource, sourceTerm, furiganaSegments, termTags, definitions: definitions2} = groupedDefinition;
- const termDetailsList = [this._createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTags)];
- const compatibilityDefinition = this._createMergedTermDefinition(
- source,
- rawSource,
- this._convertTermDefinitionsToMergedGlossaryTermDefinitions(definitions2),
- [expression],
- [reading],
- termDetailsList,
- reasons,
- score
- );
- definitionsMerged.push(compatibilityDefinition);
- }
-
- await this._buildTermMeta(definitionsMerged, enabledDictionaryMap);
- this._sortDefinitions(definitionsMerged, false);
-
- for (const definition of definitionsMerged) {
- this._flagRedundantDefinitionTags(definition.definitions);
- }
-
- return [definitionsMerged, length];
- }
-
- // Find terms internal implementation
-
- async _findTermsInternal(text, enabledDictionaryMap, options) {
- const {alphanumeric, wildcard} = options;
- text = this._getSearchableText(text, alphanumeric);
- if (text.length === 0) {
- return [[], 0];
- }
-
- const deinflections = (
- wildcard ?
- await this._findTermWildcard(text, enabledDictionaryMap, wildcard) :
- await this._findTermDeinflections(text, enabledDictionaryMap, options)
- );
-
- let maxLength = 0;
- const definitions = [];
- for (const {databaseDefinitions, source, rawSource, term, reasons} of deinflections) {
- if (databaseDefinitions.length === 0) { continue; }
- maxLength = Math.max(maxLength, rawSource.length);
- for (const databaseDefinition of databaseDefinitions) {
- const definition = await this._createTermDefinitionFromDatabaseDefinition(databaseDefinition, source, rawSource, term, reasons, enabledDictionaryMap);
- definitions.push(definition);
- }
- }
-
- this._removeDuplicateDefinitions(definitions);
- return [definitions, maxLength];
- }
-
- async _findTermWildcard(text, enabledDictionaryMap, wildcard) {
- const databaseDefinitions = await this._database.findTermsBulk([text], enabledDictionaryMap, wildcard);
- if (databaseDefinitions.length === 0) {
- return [];
- }
-
- return [{
- source: text,
- rawSource: text,
- term: text,
- rules: 0,
- reasons: [],
- databaseDefinitions
- }];
- }
-
- async _findTermDeinflections(text, enabledDictionaryMap, options) {
- const deinflections = this._getAllDeinflections(text, options);
-
- if (deinflections.length === 0) {
- return [];
- }
-
- const uniqueDeinflectionTerms = [];
- const uniqueDeinflectionArrays = [];
- const uniqueDeinflectionsMap = new Map();
- for (const deinflection of deinflections) {
- const term = deinflection.term;
- let deinflectionArray = uniqueDeinflectionsMap.get(term);
- if (typeof deinflectionArray === 'undefined') {
- deinflectionArray = [];
- uniqueDeinflectionTerms.push(term);
- uniqueDeinflectionArrays.push(deinflectionArray);
- uniqueDeinflectionsMap.set(term, deinflectionArray);
- }
- deinflectionArray.push(deinflection);
- }
-
- const databaseDefinitions = await this._database.findTermsBulk(uniqueDeinflectionTerms, enabledDictionaryMap, null);
-
- for (const databaseDefinition of databaseDefinitions) {
- const definitionRules = Deinflector.rulesToRuleFlags(databaseDefinition.rules);
- for (const deinflection of uniqueDeinflectionArrays[databaseDefinition.index]) {
- const deinflectionRules = deinflection.rules;
- if (deinflectionRules === 0 || (definitionRules & deinflectionRules) !== 0) {
- deinflection.databaseDefinitions.push(databaseDefinition);
- }
- }
- }
-
- return deinflections;
- }
-
- _getAllDeinflections(text, options) {
- const textOptionVariantArray = [
- this._getTextReplacementsVariants(options),
- this._getTextOptionEntryVariants(options.convertHalfWidthCharacters),
- this._getTextOptionEntryVariants(options.convertNumericCharacters),
- this._getTextOptionEntryVariants(options.convertAlphabeticCharacters),
- this._getTextOptionEntryVariants(options.convertHiraganaToKatakana),
- this._getTextOptionEntryVariants(options.convertKatakanaToHiragana),
- this._getCollapseEmphaticOptions(options)
- ];
-
- const jp = this._japaneseUtil;
- const deinflections = [];
- const used = new Set();
- for (const [textReplacements, halfWidth, numeric, alphabetic, katakana, hiragana, [collapseEmphatic, collapseEmphaticFull]] of this._getArrayVariants(textOptionVariantArray)) {
- let text2 = text;
- const sourceMap = new TextSourceMap(text2);
- if (textReplacements !== null) {
- text2 = this._applyTextReplacements(text2, sourceMap, textReplacements);
- }
- if (halfWidth) {
- text2 = jp.convertHalfWidthKanaToFullWidth(text2, sourceMap);
- }
- if (numeric) {
- text2 = jp.convertNumericToFullWidth(text2);
- }
- if (alphabetic) {
- text2 = jp.convertAlphabeticToKana(text2, sourceMap);
- }
- if (katakana) {
- text2 = jp.convertHiraganaToKatakana(text2);
- }
- if (hiragana) {
- text2 = jp.convertKatakanaToHiragana(text2);
- }
- if (collapseEmphatic) {
- text2 = jp.collapseEmphaticSequences(text2, collapseEmphaticFull, sourceMap);
- }
-
- for (let i = text2.length; i > 0; --i) {
- const text2Substring = text2.substring(0, i);
- if (used.has(text2Substring)) { break; }
- used.add(text2Substring);
- const rawSource = sourceMap.source.substring(0, sourceMap.getSourceLength(i));
- for (const deinflection of this._deinflector.deinflect(text2Substring, rawSource)) {
- deinflections.push(deinflection);
- }
- }
- }
- return deinflections;
- }
-
- async _getSequencedDefinitions(definitions, mainDictionary, enabledDictionaryMap) {
- const sequenceList = [];
- const sequencedDefinitionMap = new Map();
- const sequencedDefinitions = [];
- const unsequencedDefinitions = [];
- for (const definition of definitions) {
- const {sequence, dictionary} = definition;
- if (mainDictionary === dictionary && sequence >= 0) {
- let sequencedDefinition = sequencedDefinitionMap.get(sequence);
- if (typeof sequencedDefinition === 'undefined') {
- sequencedDefinition = {
- sourceDefinitions: [],
- relatedDefinitions: [],
- relatedDefinitionIds: new Set()
- };
- sequencedDefinitionMap.set(sequence, sequencedDefinition);
- sequencedDefinitions.push(sequencedDefinition);
- sequenceList.push(sequence);
- }
- sequencedDefinition.sourceDefinitions.push(definition);
- sequencedDefinition.relatedDefinitions.push(definition);
- sequencedDefinition.relatedDefinitionIds.add(definition.id);
- } else {
- unsequencedDefinitions.push(definition);
- }
- }
-
- if (sequenceList.length > 0) {
- const databaseDefinitions = await this._database.findTermsBySequenceBulk(sequenceList, mainDictionary);
- for (const databaseDefinition of databaseDefinitions) {
- const {relatedDefinitions, relatedDefinitionIds} = sequencedDefinitions[databaseDefinition.index];
- const {id} = databaseDefinition;
- if (relatedDefinitionIds.has(id)) { continue; }
-
- const {source, rawSource, sourceTerm} = relatedDefinitions[0];
- const definition = await this._createTermDefinitionFromDatabaseDefinition(databaseDefinition, source, rawSource, sourceTerm, [], enabledDictionaryMap);
- relatedDefinitions.push(definition);
- }
- }
-
- for (const {relatedDefinitions} of sequencedDefinitions) {
- this._sortDefinitionsById(relatedDefinitions);
- }
-
- return {sequencedDefinitions, unsequencedDefinitions};
- }
-
- async _getMergedSecondarySearchResults(expressionsMap, secondarySearchDictionaryMap) {
- if (secondarySearchDictionaryMap.size === 0) {
- return [];
- }
-
- const expressionList = [];
- const readingList = [];
- for (const [expression, readingMap] of expressionsMap.entries()) {
- for (const reading of readingMap.keys()) {
- expressionList.push(expression);
- readingList.push(reading);
- }
- }
-
- const databaseDefinitions = await this._database.findTermsExactBulk(expressionList, readingList, secondarySearchDictionaryMap);
- this._sortDatabaseDefinitionsByIndex(databaseDefinitions);
-
- const definitions = [];
- for (const databaseDefinition of databaseDefinitions) {
- const source = expressionList[databaseDefinition.index];
- const definition = await this._createTermDefinitionFromDatabaseDefinition(databaseDefinition, source, source, source, [], secondarySearchDictionaryMap);
- definitions.push(definition);
- }
-
- return definitions;
- }
-
- async _getMergedDefinition(sourceDefinitions, relatedDefinitions, unsequencedDefinitions, secondarySearchDictionaryMap, usedDefinitions) {
- const {reasons, source, rawSource} = sourceDefinitions[0];
- const score = this._getMaxDefinitionScore(sourceDefinitions);
- const termInfoMap = new Map();
- const glossaryDefinitions = [];
- const glossaryDefinitionGroupMap = new Map();
-
- this._mergeByGlossary(relatedDefinitions, glossaryDefinitionGroupMap);
- this._addUniqueTermInfos(relatedDefinitions, termInfoMap);
-
- let secondaryDefinitions = await this._getMergedSecondarySearchResults(termInfoMap, secondarySearchDictionaryMap);
- secondaryDefinitions = [...unsequencedDefinitions, ...secondaryDefinitions];
-
- this._removeUsedDefinitions(secondaryDefinitions, termInfoMap, usedDefinitions);
- this._removeDuplicateDefinitions(secondaryDefinitions);
-
- this._mergeByGlossary(secondaryDefinitions, glossaryDefinitionGroupMap);
-
- const allExpressions = new Set();
- const allReadings = new Set();
- for (const {expressions, readings} of glossaryDefinitionGroupMap.values()) {
- for (const expression of expressions) { allExpressions.add(expression); }
- for (const reading of readings) { allReadings.add(reading); }
- }
-
- for (const {expressions, readings, definitions} of glossaryDefinitionGroupMap.values()) {
- const glossaryDefinition = this._createMergedGlossaryTermDefinition(
- source,
- rawSource,
- definitions,
- expressions,
- readings,
- allExpressions,
- allReadings
- );
- glossaryDefinitions.push(glossaryDefinition);
- }
-
- this._sortDefinitions(glossaryDefinitions, true);
-
- const termDetailsList = this._createTermDetailsListFromTermInfoMap(termInfoMap);
-
- return this._createMergedTermDefinition(
- source,
- rawSource,
- glossaryDefinitions,
- [...allExpressions],
- [...allReadings],
- termDetailsList,
- reasons,
- score
- );
- }
-
- _removeUsedDefinitions(definitions, termInfoMap, usedDefinitions) {
- for (let i = 0, ii = definitions.length; i < ii; ++i) {
- const definition = definitions[i];
- const {expression, reading} = definition;
- const expressionMap = termInfoMap.get(expression);
- if (
- typeof expressionMap !== 'undefined' &&
- typeof expressionMap.get(reading) !== 'undefined'
- ) {
- usedDefinitions.add(definition);
- } else {
- definitions.splice(i, 1);
- --i;
- --ii;
- }
- }
- }
-
- _getUniqueDefinitionTags(definitions) {
- const definitionTagsMap = new Map();
- for (const {definitionTags} of definitions) {
- for (const tag of definitionTags) {
- const {name} = tag;
- if (definitionTagsMap.has(name)) { continue; }
- definitionTagsMap.set(name, this._cloneTag(tag));
- }
- }
- return [...definitionTagsMap.values()];
- }
-
- _removeDuplicateDefinitions(definitions) {
- const definitionGroups = new Map();
- for (let i = 0, ii = definitions.length; i < ii; ++i) {
- const definition = definitions[i];
- const {id} = definition;
- const existing = definitionGroups.get(id);
- if (typeof existing === 'undefined') {
- definitionGroups.set(id, [i, definition]);
- continue;
- }
-
- let removeIndex = i;
- if (definition.source.length > existing[1].source.length) {
- definitionGroups.set(id, [i, definition]);
- removeIndex = existing[0];
- }
-
- definitions.splice(removeIndex, 1);
- --i;
- --ii;
- }
- }
-
- _flagRedundantDefinitionTags(definitions) {
- let lastDictionary = null;
- let lastPartOfSpeech = '';
- const removeCategoriesSet = new Set();
-
- for (const {dictionary, definitionTags} of definitions) {
- const partOfSpeech = this._createMapKey(this._getTagNamesWithCategory(definitionTags, 'partOfSpeech'));
-
- if (lastDictionary !== dictionary) {
- lastDictionary = dictionary;
- lastPartOfSpeech = '';
- }
-
- if (lastPartOfSpeech === partOfSpeech) {
- removeCategoriesSet.add('partOfSpeech');
- } else {
- lastPartOfSpeech = partOfSpeech;
- }
-
- if (removeCategoriesSet.size > 0) {
- this._flagTagsWithCategoryAsRedundant(definitionTags, removeCategoriesSet);
- removeCategoriesSet.clear();
- }
- }
- }
-
- _groupTerms(definitions) {
- const groups = new Map();
- for (const definition of definitions) {
- const key = this._createMapKey([definition.source, definition.expression, definition.reading, ...definition.reasons]);
- let groupDefinitions = groups.get(key);
- if (typeof groupDefinitions === 'undefined') {
- groupDefinitions = [];
- groups.set(key, groupDefinitions);
- }
-
- groupDefinitions.push(definition);
- }
-
- const results = [];
- for (const groupDefinitions of groups.values()) {
- this._sortDefinitions(groupDefinitions, true);
- const definition = this._createGroupedTermDefinition(groupDefinitions);
- results.push(definition);
- }
-
- return results;
- }
-
- _mergeByGlossary(definitions, glossaryDefinitionGroupMap) {
- for (const definition of definitions) {
- const {expression, reading, dictionary, glossary, id} = definition;
-
- const key = this._createMapKey([dictionary, ...glossary]);
- let group = glossaryDefinitionGroupMap.get(key);
- if (typeof group === 'undefined') {
- group = {
- expressions: new Set(),
- readings: new Set(),
- definitions: [],
- definitionIds: new Set()
- };
- glossaryDefinitionGroupMap.set(key, group);
- }
-
- const {definitionIds} = group;
- if (definitionIds.has(id)) { continue; }
- definitionIds.add(id);
- group.expressions.add(expression);
- group.readings.add(reading);
- group.definitions.push(definition);
- }
- }
-
- _addUniqueTermInfos(definitions, termInfoMap) {
- for (const {expression, reading, sourceTerm, furiganaSegments, termTags} of definitions) {
- let readingMap = termInfoMap.get(expression);
- if (typeof readingMap === 'undefined') {
- readingMap = new Map();
- termInfoMap.set(expression, readingMap);
- }
-
- let termInfo = readingMap.get(reading);
- if (typeof termInfo === 'undefined') {
- termInfo = {
- sourceTerm,
- furiganaSegments,
- termTagsMap: new Map()
- };
- readingMap.set(reading, termInfo);
- }
-
- const {termTagsMap} = termInfo;
- for (const tag of termTags) {
- const {name} = tag;
- if (termTagsMap.has(name)) { continue; }
- termTagsMap.set(name, this._cloneTag(tag));
- }
- }
- }
-
- _convertTermDefinitionsToMergedGlossaryTermDefinitions(definitions) {
- const convertedDefinitions = [];
- for (const definition of definitions) {
- const {source, rawSource, expression, reading} = definition;
- const expressions = new Set([expression]);
- const readings = new Set([reading]);
- const convertedDefinition = this._createMergedGlossaryTermDefinition(source, rawSource, [definition], expressions, readings, expressions, readings);
- convertedDefinitions.push(convertedDefinition);
- }
- return convertedDefinitions;
- }
-
- // Metadata building
-
- async _buildTermMeta(definitions, enabledDictionaryMap) {
- const addMetadataTargetInfo = (targetMap1, target, parents) => {
- let {expression, reading} = target;
- if (!reading) { reading = expression; }
-
- let targetMap2 = targetMap1.get(expression);
- if (typeof targetMap2 === 'undefined') {
- targetMap2 = new Map();
- targetMap1.set(expression, targetMap2);
- }
-
- let targets = targetMap2.get(reading);
- if (typeof targets === 'undefined') {
- targets = new Set([target, ...parents]);
- targetMap2.set(reading, targets);
- } else {
- targets.add(target);
- for (const parent of parents) {
- targets.add(parent);
- }
- }
- };
-
- const targetMap = new Map();
- const definitionsQueue = definitions.map((definition) => ({definition, parents: []}));
- while (definitionsQueue.length > 0) {
- const {definition, parents} = definitionsQueue.shift();
- const childDefinitions = definition.definitions;
- if (Array.isArray(childDefinitions)) {
- for (const definition2 of childDefinitions) {
- definitionsQueue.push({definition: definition2, parents: [...parents, definition]});
- }
- } else {
- addMetadataTargetInfo(targetMap, definition, parents);
- }
-
- for (const target of definition.expressions) {
- addMetadataTargetInfo(targetMap, target, []);
- }
- }
- const targetMapEntries = [...targetMap.entries()];
- const uniqueExpressions = targetMapEntries.map(([expression]) => expression);
-
- const metas = await this._database.findTermMetaBulk(uniqueExpressions, enabledDictionaryMap);
- for (const {expression, mode, data, dictionary, index} of metas) {
- const targetMap2 = targetMapEntries[index][1];
- for (const [reading, targets] of targetMap2) {
- switch (mode) {
- case 'freq':
- {
- const frequencyData = this._getTermFrequencyData(expression, reading, dictionary, data);
- if (frequencyData === null) { continue; }
- for (const {frequencies} of targets) { frequencies.push(frequencyData); }
- }
- break;
- case 'pitch':
- {
- const pitchData = await this._getPitchData(expression, reading, dictionary, data);
- if (pitchData === null) { continue; }
- for (const {pitches} of targets) { pitches.push(pitchData); }
- }
- break;
- }
- }
- }
- }
-
- async _buildKanjiMeta(definitions, enabledDictionaryMap) {
- const kanjiList = [];
- for (const {character} of definitions) {
- kanjiList.push(character);
- }
-
- const metas = await this._database.findKanjiMetaBulk(kanjiList, enabledDictionaryMap);
- for (const {character, mode, data, dictionary, index} of metas) {
- switch (mode) {
- case 'freq':
- {
- const frequencyData = this._getKanjiFrequencyData(character, dictionary, data);
- definitions[index].frequencies.push(frequencyData);
- }
- break;
- }
- }
- }
-
- async _expandTags(names, dictionary) {
- const tagMetaList = await this._getTagMetaList(names, dictionary);
- const results = [];
- for (let i = 0, ii = tagMetaList.length; i < ii; ++i) {
- const meta = tagMetaList[i];
- const name = names[i];
- const {category, notes, order, score} = (meta !== null ? meta : {});
- const tag = this._createTag(name, category, notes, order, score, dictionary, false);
- results.push(tag);
- }
- return results;
- }
-
- async _expandStats(items, dictionary) {
- const names = Object.keys(items);
- const tagMetaList = await this._getTagMetaList(names, dictionary);
-
- const statsGroups = new Map();
- for (let i = 0; i < names.length; ++i) {
- const name = names[i];
- const meta = tagMetaList[i];
- if (meta === null) { continue; }
-
- const {category, notes, order, score} = meta;
- let group = statsGroups.get(category);
- if (typeof group === 'undefined') {
- group = [];
- statsGroups.set(category, group);
- }
-
- const value = items[name];
- const stat = this._createKanjiStat(name, category, notes, order, score, dictionary, value);
- group.push(stat);
- }
-
- const stats = {};
- for (const [category, group] of statsGroups.entries()) {
- this._sortKanjiStats(group);
- stats[category] = group;
- }
- return stats;
- }
-
- async _getTagMetaList(names, dictionary) {
- const tagMetaList = [];
- let cache = this._tagCache.get(dictionary);
- if (typeof cache === 'undefined') {
- cache = new Map();
- this._tagCache.set(dictionary, cache);
- }
-
- for (const name of names) {
- const base = this._getNameBase(name);
-
- let tagMeta = cache.get(base);
- if (typeof tagMeta === 'undefined') {
- tagMeta = await this._database.findTagForTitle(base, dictionary);
- cache.set(base, tagMeta);
- }
-
- tagMetaList.push(tagMeta);
- }
-
- return tagMetaList;
- }
-
- _getTermFrequencyData(expression, reading, dictionary, data) {
- let frequency = data;
- const hasReading = (data !== null && typeof data === 'object');
- if (hasReading) {
- if (data.reading !== reading) { return null; }
- frequency = data.frequency;
- }
- return {dictionary, expression, reading, hasReading, frequency};
- }
-
- _getKanjiFrequencyData(character, dictionary, data) {
- return {dictionary, character, frequency: data};
- }
-
- async _getPitchData(expression, reading, dictionary, data) {
- if (data.reading !== reading) { return null; }
-
- const pitches = [];
- for (let {position, tags} of data.pitches) {
- tags = Array.isArray(tags) ? await this._expandTags(tags, dictionary) : [];
- pitches.push({position, tags});
- }
-
- return {expression, reading, dictionary, pitches};
- }
-
- // Simple helpers
-
- _scoreToTermFrequency(score) {
- if (score > 0) {
- return 'popular';
- } else if (score < 0) {
- return 'rare';
- } else {
- return 'normal';
- }
- }
-
- _getNameBase(name) {
- const pos = name.indexOf(':');
- return (pos >= 0 ? name.substring(0, pos) : name);
- }
-
- _getSearchableText(text, allowAlphanumericCharacters) {
- if (allowAlphanumericCharacters) {
- return text;
- }
-
- const jp = this._japaneseUtil;
- let newText = '';
- for (const c of text) {
- if (!jp.isCodePointJapanese(c.codePointAt(0))) {
- break;
- }
- newText += c;
- }
- return newText;
- }
-
- _getTextOptionEntryVariants(value) {
- switch (value) {
- case 'true': return [true];
- case 'variant': return [false, true];
- default: return [false];
- }
- }
-
- _getCollapseEmphaticOptions(options) {
- const collapseEmphaticOptions = [[false, false]];
- switch (options.collapseEmphaticSequences) {
- case 'true':
- collapseEmphaticOptions.push([true, false]);
- break;
- case 'full':
- collapseEmphaticOptions.push([true, false], [true, true]);
- break;
- }
- return collapseEmphaticOptions;
- }
-
- _getTextReplacementsVariants(options) {
- return options.textReplacements;
- }
-
- _getSecondarySearchDictionaryMap(enabledDictionaryMap) {
- const secondarySearchDictionaryMap = new Map();
- for (const [dictionary, details] of enabledDictionaryMap.entries()) {
- if (!details.allowSecondarySearches) { continue; }
- secondarySearchDictionaryMap.set(dictionary, details);
- }
- return secondarySearchDictionaryMap;
- }
-
- _getDictionaryPriority(dictionary, enabledDictionaryMap) {
- const info = enabledDictionaryMap.get(dictionary);
- return typeof info !== 'undefined' ? info.priority : 0;
- }
-
- _getTagNamesWithCategory(tags, category) {
- const results = [];
- for (const tag of tags) {
- if (tag.category !== category) { continue; }
- results.push(tag.name);
- }
- results.sort();
- return results;
- }
-
- _flagTagsWithCategoryAsRedundant(tags, removeCategoriesSet) {
- for (const tag of tags) {
- if (removeCategoriesSet.has(tag.category)) {
- tag.redundant = true;
- }
- }
- }
-
- _getUniqueDictionaryNames(definitions) {
- const uniqueDictionaryNames = new Set();
- for (const {dictionaryNames} of definitions) {
- for (const dictionaryName of dictionaryNames) {
- uniqueDictionaryNames.add(dictionaryName);
- }
- }
- return [...uniqueDictionaryNames];
- }
-
- _getUniqueTermTags(definitions) {
- const newTermTags = [];
- if (definitions.length <= 1) {
- for (const {termTags} of definitions) {
- for (const tag of termTags) {
- newTermTags.push(this._cloneTag(tag));
- }
- }
- } else {
- const tagsSet = new Set();
- let checkTagsMap = false;
- for (const {termTags} of definitions) {
- for (const tag of termTags) {
- const key = this._getTagMapKey(tag);
- if (checkTagsMap && tagsSet.has(key)) { continue; }
- tagsSet.add(key);
- newTermTags.push(this._cloneTag(tag));
- }
- checkTagsMap = true;
- }
- }
- return newTermTags;
- }
-
- *_getArrayVariants(arrayVariants) {
- const ii = arrayVariants.length;
-
- let total = 1;
- for (let i = 0; i < ii; ++i) {
- total *= arrayVariants[i].length;
- }
-
- for (let a = 0; a < total; ++a) {
- const variant = [];
- let index = a;
- for (let i = 0; i < ii; ++i) {
- const entryVariants = arrayVariants[i];
- variant.push(entryVariants[index % entryVariants.length]);
- index = Math.floor(index / entryVariants.length);
- }
- yield variant;
- }
- }
-
- _areSetsEqual(set1, set2) {
- if (set1.size !== set2.size) {
- return false;
- }
-
- for (const value of set1) {
- if (!set2.has(value)) {
- return false;
- }
- }
-
- return true;
- }
-
- _getSetIntersection(set1, set2) {
- const result = [];
- for (const value of set1) {
- if (set2.has(value)) {
- result.push(value);
- }
- }
- return result;
- }
-
- // Reduction functions
-
- _getTermTagsScoreSum(termTags) {
- let result = 0;
- for (const {score} of termTags) {
- result += score;
- }
- return result;
- }
-
- _getSourceTermMatchCountSum(definitions) {
- let result = 0;
- for (const {sourceTermExactMatchCount} of definitions) {
- result += sourceTermExactMatchCount;
- }
- return result;
- }
-
- _getMaxDefinitionScore(definitions) {
- let result = Number.MIN_SAFE_INTEGER;
- for (const {score} of definitions) {
- if (score > result) { result = score; }
- }
- return result;
- }
-
- _getMaxDictionaryPriority(definitions) {
- let result = Number.MIN_SAFE_INTEGER;
- for (const {dictionaryPriority} of definitions) {
- if (dictionaryPriority > result) { result = dictionaryPriority; }
- }
- return result;
- }
-
- // Common data creation and cloning functions
-
- _cloneTag(tag) {
- const {name, category, notes, order, score, dictionary, redundant} = tag;
- return this._createTag(name, category, notes, order, score, dictionary, redundant);
- }
-
- _getTagMapKey(tag) {
- const {name, category, notes} = tag;
- return this._createMapKey([name, category, notes]);
- }
-
- _createMapKey(array) {
- return JSON.stringify(array);
- }
-
- _createTag(name, category, notes, order, score, dictionary, redundant) {
- return {
- name,
- category: (typeof category === 'string' && category.length > 0 ? category : 'default'),
- notes: (typeof notes === 'string' ? notes : ''),
- order: (typeof order === 'number' ? order : 0),
- score: (typeof score === 'number' ? score : 0),
- dictionary: (typeof dictionary === 'string' ? dictionary : null),
- redundant
- };
- }
-
- _createKanjiStat(name, category, notes, order, score, dictionary, value) {
- return {
- name,
- category: (typeof category === 'string' && category.length > 0 ? category : 'default'),
- notes: (typeof notes === 'string' ? notes : ''),
- order: (typeof order === 'number' ? order : 0),
- score: (typeof score === 'number' ? score : 0),
- dictionary: (typeof dictionary === 'string' ? dictionary : null),
- value
- };
- }
-
- _createKanjiDefinition(character, dictionary, onyomi, kunyomi, glossary, tags, stats) {
- return {
- type: 'kanji',
- character,
- dictionary,
- onyomi,
- kunyomi,
- glossary,
- tags,
- stats,
- frequencies: []
- };
- }
-
- async _createTermDefinitionFromDatabaseDefinition(databaseDefinition, source, rawSource, sourceTerm, reasons, enabledDictionaryMap) {
- const {expression, reading, definitionTags, termTags, glossary, score, dictionary, id, sequence} = databaseDefinition;
- const dictionaryPriority = this._getDictionaryPriority(dictionary, enabledDictionaryMap);
- const termTagsExpanded = await this._expandTags(termTags, dictionary);
- const definitionTagsExpanded = await this._expandTags(definitionTags, dictionary);
-
- this._sortTags(definitionTagsExpanded);
- this._sortTags(termTagsExpanded);
-
- const furiganaSegments = this._japaneseUtil.distributeFurigana(expression, reading);
- const termDetailsList = [this._createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTagsExpanded)];
- const sourceTermExactMatchCount = (sourceTerm === expression ? 1 : 0);
-
- return {
- type: 'term',
- id,
- source,
- rawSource,
- sourceTerm,
- reasons,
- score,
- sequence,
- dictionary,
- dictionaryPriority,
- dictionaryNames: [dictionary],
- expression,
- reading,
- expressions: termDetailsList,
- furiganaSegments,
- glossary,
- definitionTags: definitionTagsExpanded,
- termTags: termTagsExpanded,
- // definitions
- frequencies: [],
- pitches: [],
- // only
- sourceTermExactMatchCount
- };
- }
-
- _createGroupedTermDefinition(definitions) {
- const {expression, reading, furiganaSegments, reasons, source, rawSource, sourceTerm} = definitions[0];
- const score = this._getMaxDefinitionScore(definitions);
- const dictionaryPriority = this._getMaxDictionaryPriority(definitions);
- const dictionaryNames = this._getUniqueDictionaryNames(definitions);
- const termTags = this._getUniqueTermTags(definitions);
- const termDetailsList = [this._createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTags)];
- const sourceTermExactMatchCount = (sourceTerm === expression ? 1 : 0);
- return {
- type: 'termGrouped',
- // id
- source,
- rawSource,
- sourceTerm,
- reasons: [...reasons],
- score,
- // sequence
- dictionary: dictionaryNames[0],
- dictionaryPriority,
- dictionaryNames,
- expression,
- reading,
- expressions: termDetailsList,
- furiganaSegments, // Contains duplicate data
- // glossary
- // definitionTags
- termTags,
- definitions, // type: 'term'
- frequencies: [],
- pitches: [],
- // only
- sourceTermExactMatchCount
- };
- }
-
- _createMergedTermDefinition(source, rawSource, definitions, expressions, readings, termDetailsList, reasons, score) {
- const dictionaryPriority = this._getMaxDictionaryPriority(definitions);
- const sourceTermExactMatchCount = this._getSourceTermMatchCountSum(definitions);
- const dictionaryNames = this._getUniqueDictionaryNames(definitions);
- return {
- type: 'termMerged',
- // id
- source,
- rawSource,
- // sourceTerm
- reasons,
- score,
- // sequence
- dictionary: dictionaryNames[0],
- dictionaryPriority,
- dictionaryNames,
- expression: expressions,
- reading: readings,
- expressions: termDetailsList,
- // furiganaSegments
- // glossary
- // definitionTags
- // termTags
- definitions, // type: 'termMergedByGlossary'
- frequencies: [],
- pitches: [],
- // only
- sourceTermExactMatchCount
- };
- }
-
- _createMergedGlossaryTermDefinition(source, rawSource, definitions, expressions, readings, allExpressions, allReadings) {
- const only = [];
- if (!this._areSetsEqual(expressions, allExpressions)) {
- only.push(...this._getSetIntersection(expressions, allExpressions));
- }
- if (!this._areSetsEqual(readings, allReadings)) {
- only.push(...this._getSetIntersection(readings, allReadings));
- }
-
- const sourceTermExactMatchCount = this._getSourceTermMatchCountSum(definitions);
- const dictionaryNames = this._getUniqueDictionaryNames(definitions);
-
- const termInfoMap = new Map();
- this._addUniqueTermInfos(definitions, termInfoMap);
- const termDetailsList = this._createTermDetailsListFromTermInfoMap(termInfoMap);
-
- const definitionTags = this._getUniqueDefinitionTags(definitions);
- this._sortTags(definitionTags);
-
- const {glossary} = definitions[0];
- const score = this._getMaxDefinitionScore(definitions);
- const dictionaryPriority = this._getMaxDictionaryPriority(definitions);
- return {
- type: 'termMergedByGlossary',
- // id
- source,
- rawSource,
- // sourceTerm
- reasons: [],
- score,
- // sequence
- dictionary: dictionaryNames[0],
- dictionaryPriority,
- dictionaryNames,
- expression: [...expressions],
- reading: [...readings],
- expressions: termDetailsList,
- // furiganaSegments
- glossary: [...glossary],
- definitionTags,
- // termTags
- definitions, // type: 'term'; contains duplicate data
- frequencies: [],
- pitches: [],
- only,
- sourceTermExactMatchCount
- };
- }
-
- _createTermDetailsListFromTermInfoMap(termInfoMap) {
- const termDetailsList = [];
- for (const [expression, readingMap] of termInfoMap.entries()) {
- for (const [reading, {termTagsMap, sourceTerm, furiganaSegments}] of readingMap.entries()) {
- const termTags = [...termTagsMap.values()];
- this._sortTags(termTags);
- termDetailsList.push(this._createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTags));
- }
- }
- return termDetailsList;
- }
-
- _createTermDetails(sourceTerm, expression, reading, furiganaSegments, termTags) {
- const termFrequency = this._scoreToTermFrequency(this._getTermTagsScoreSum(termTags));
- return {
- sourceTerm,
- expression,
- reading,
- furiganaSegments, // Contains duplicate data
- termTags,
- termFrequency,
- frequencies: [],
- pitches: []
- };
- }
-
- // Sorting functions
-
- _sortTags(tags) {
- if (tags.length <= 1) { return; }
- const stringComparer = this._stringComparer;
- tags.sort((v1, v2) => {
- const i = v1.order - v2.order;
- if (i !== 0) { return i; }
-
- return stringComparer.compare(v1.name, v2.name);
- });
- }
-
- _sortDefinitions(definitions, useDictionaryPriority) {
- if (definitions.length <= 1) { return; }
- const stringComparer = this._stringComparer;
- const compareFunction1 = (v1, v2) => {
- let i = v2.source.length - v1.source.length;
- if (i !== 0) { return i; }
-
- i = v1.reasons.length - v2.reasons.length;
- if (i !== 0) { return i; }
-
- i = v2.sourceTermExactMatchCount - v1.sourceTermExactMatchCount;
- if (i !== 0) { return i; }
-
- i = v2.score - v1.score;
- if (i !== 0) { return i; }
-
- const expression1 = v1.expression;
- const expression2 = v2.expression;
- if (typeof expression1 !== 'string' || typeof expression2 !== 'string') { return 0; } // Skip if either is not a string (array)
-
- i = expression2.length - expression1.length;
- if (i !== 0) { return i; }
-
- return stringComparer.compare(expression1, expression2);
- };
- const compareFunction2 = (v1, v2) => {
- const i = v2.dictionaryPriority - v1.dictionaryPriority;
- return (i !== 0) ? i : compareFunction1(v1, v2);
- };
- definitions.sort(useDictionaryPriority ? compareFunction2 : compareFunction1);
- }
-
- _sortDatabaseDefinitionsByIndex(definitions) {
- if (definitions.length <= 1) { return; }
- definitions.sort((a, b) => a.index - b.index);
- }
-
- _sortDefinitionsById(definitions) {
- if (definitions.length <= 1) { return; }
- definitions.sort((a, b) => a.id - b.id);
- }
-
- _sortKanjiStats(stats) {
- if (stats.length <= 1) { return; }
- const stringComparer = this._stringComparer;
- stats.sort((v1, v2) => {
- const i = v1.order - v2.order;
- if (i !== 0) { return i; }
-
- return stringComparer.compare(v1.notes, v2.notes);
- });
- }
-
- // Regex functions
-
- _applyTextReplacements(text, sourceMap, replacements) {
- for (const {pattern, replacement} of replacements) {
- text = this._applyTextReplacement(text, sourceMap, pattern, replacement);
- }
- return text;
- }
-
- _applyTextReplacement(text, sourceMap, pattern, replacement) {
- const isGlobal = pattern.global;
- if (isGlobal) { pattern.lastIndex = 0; }
- for (let loop = true; loop; loop = isGlobal) {
- const match = pattern.exec(text);
- if (match === null) { break; }
-
- const matchText = match[0];
- const index = match.index;
- const actualReplacement = this._applyMatchReplacement(replacement, match);
- const actualReplacementLength = actualReplacement.length;
- const delta = actualReplacementLength - (matchText.length > 0 ? matchText.length : -1);
-
- text = `${text.substring(0, index)}${actualReplacement}${text.substring(index + matchText.length)}`;
- pattern.lastIndex += delta;
-
- if (actualReplacementLength > 0) {
- sourceMap.combine(Math.max(0, index - 1), matchText.length);
- sourceMap.insert(index, ...(new Array(actualReplacementLength).fill(0)));
- } else {
- sourceMap.combine(index, matchText.length);
- }
- }
- return text;
- }
-
- _applyMatchReplacement(replacement, match) {
- const pattern = /\$(?:\$|&|`|'|(\d\d?)|<([^>]*)>)/g;
- return replacement.replace(pattern, (g0, g1, g2) => {
- if (typeof g1 !== 'undefined') {
- const matchIndex = Number.parseInt(g1, 10);
- if (matchIndex >= 1 && matchIndex <= match.length) {
- return match[matchIndex];
- }
- } else if (typeof g2 !== 'undefined') {
- const {groups} = match;
- if (typeof groups === 'object' && groups !== null && Object.prototype.hasOwnProperty.call(groups, g2)) {
- return groups[g2];
- }
- } else {
- switch (g0) {
- case '$': return '$';
- case '&': return match[0];
- case '`': return replacement.substring(0, match.index);
- case '\'': return replacement.substring(match.index + g0.length);
- }
- }
- return g0;
- });
- }
-}
diff --git a/ext/bg/js/welcome-main.js b/ext/bg/js/welcome-main.js
deleted file mode 100644
index 57b265dc..00000000
--- a/ext/bg/js/welcome-main.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (C) 2019-2021 Yomichan Authors
- *
- * 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/>.
- */
-
-/* global
- * DictionaryController
- * DictionaryImportController
- * DocumentFocusController
- * GenericSettingController
- * ModalController
- * ScanInputsSimpleController
- * SettingsController
- * SettingsDisplayController
- * StatusFooter
- * api
- */
-
-async function setupEnvironmentInfo() {
- const {manifest_version: manifestVersion} = chrome.runtime.getManifest();
- const {browser, platform} = await api.getEnvironmentInfo();
- document.documentElement.dataset.browser = browser;
- document.documentElement.dataset.os = platform.os;
- document.documentElement.dataset.manifestVersion = `${manifestVersion}`;
-}
-
-async function setupGenericSettingsController(genericSettingController) {
- await genericSettingController.prepare();
- await genericSettingController.refresh();
-}
-
-(async () => {
- try {
- const documentFocusController = new DocumentFocusController();
- documentFocusController.prepare();
-
- const statusFooter = new StatusFooter(document.querySelector('.status-footer-container'));
- statusFooter.prepare();
-
- api.forwardLogsToBackend();
- await yomichan.prepare();
-
- setupEnvironmentInfo();
-
- const optionsFull = await api.optionsGetFull();
-
- const preparePromises = [];
-
- const modalController = new ModalController();
- modalController.prepare();
-
- const settingsController = new SettingsController(optionsFull.profileCurrent);
- settingsController.prepare();
-
- const dictionaryController = new DictionaryController(settingsController, modalController, null, statusFooter);
- dictionaryController.prepare();
-
- const dictionaryImportController = new DictionaryImportController(settingsController, modalController, null, statusFooter);
- dictionaryImportController.prepare();
-
- const genericSettingController = new GenericSettingController(settingsController);
- preparePromises.push(setupGenericSettingsController(genericSettingController));
-
- const simpleScanningInputController = new ScanInputsSimpleController(settingsController);
- simpleScanningInputController.prepare();
-
- await Promise.all(preparePromises);
-
- document.documentElement.dataset.loaded = 'true';
-
- const settingsDisplayController = new SettingsDisplayController(settingsController, modalController);
- settingsDisplayController.prepare();
- } catch (e) {
- yomichan.logError(e);
- }
-})();