summaryrefslogtreecommitdiff
path: root/ext/bg/js/backend.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg/js/backend.js')
-rw-r--r--ext/bg/js/backend.js2053
1 files changed, 0 insertions, 2053 deletions
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);
- }
-}