Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 224 lines 9.3 kB view raw
1/* 2 * Copyright (C) 2023-2025 Yomitan Authors 3 * Copyright (C) 2016-2022 Yomichan Authors 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <https://www.gnu.org/licenses/>. 17 */ 18 19import {API} from '../comm/api.js'; 20import {ClipboardReader} from '../comm/clipboard-reader.js'; 21import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; 22import {ExtensionError} from '../core/extension-error.js'; 23import {log} from '../core/log.js'; 24import {sanitizeCSS} from '../core/utilities.js'; 25import {arrayBufferToBase64} from '../data/array-buffer-util.js'; 26import {DictionaryDatabase} from '../dictionary/dictionary-database.js'; 27import {WebExtension} from '../extension/web-extension.js'; 28import {Translator} from '../language/translator.js'; 29 30/** 31 * This class controls the core logic of the extension, including API calls 32 * and various forms of communication between browser tabs and external applications. 33 */ 34export class Offscreen { 35 /** 36 * Creates a new instance. 37 */ 38 constructor() { 39 /** @type {DictionaryDatabase} */ 40 this._dictionaryDatabase = new DictionaryDatabase(); 41 /** @type {Translator} */ 42 this._translator = new Translator(this._dictionaryDatabase); 43 /** @type {ClipboardReader} */ 44 this._clipboardReader = new ClipboardReader( 45 (typeof document === 'object' && document !== null ? document : null), 46 '#clipboard-paste-target', 47 '#clipboard-rich-content-paste-target', 48 ); 49 50 /* eslint-disable @stylistic/no-multi-spaces */ 51 /** @type {import('offscreen').ApiMap} */ 52 this._apiMap = createApiMap([ 53 ['clipboardGetTextOffscreen', this._getTextHandler.bind(this)], 54 ['clipboardGetImageOffscreen', this._getImageHandler.bind(this)], 55 ['clipboardSetBrowserOffscreen', this._setClipboardBrowser.bind(this)], 56 ['databasePrepareOffscreen', this._prepareDatabaseHandler.bind(this)], 57 ['getDictionaryInfoOffscreen', this._getDictionaryInfoHandler.bind(this)], 58 ['databasePurgeOffscreen', this._purgeDatabaseHandler.bind(this)], 59 ['databaseGetMediaOffscreen', this._getMediaHandler.bind(this)], 60 ['translatorPrepareOffscreen', this._prepareTranslatorHandler.bind(this)], 61 ['findKanjiOffscreen', this._findKanjiHandler.bind(this)], 62 ['findTermsOffscreen', this._findTermsHandler.bind(this)], 63 ['getTermFrequenciesOffscreen', this._getTermFrequenciesHandler.bind(this)], 64 ['clearDatabaseCachesOffscreen', this._clearDatabaseCachesHandler.bind(this)], 65 ['createAndRegisterPortOffscreen', this._createAndRegisterPort.bind(this)], 66 ['sanitizeCSSOffscreen', this._sanitizeCSSOffscreen.bind(this)], 67 ]); 68 /* eslint-enable @stylistic/no-multi-spaces */ 69 70 /** @type {import('offscreen').McApiMap} */ 71 this._mcApiMap = createApiMap([ 72 ['connectToDatabaseWorker', this._connectToDatabaseWorkerHandler.bind(this)], 73 ]); 74 75 /** @type {?Promise<void>} */ 76 this._prepareDatabasePromise = null; 77 78 /** 79 * @type {API} 80 */ 81 this._api = new API(new WebExtension()); 82 } 83 84 /** */ 85 prepare() { 86 chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); 87 navigator.serviceWorker.addEventListener('controllerchange', this._createAndRegisterPort.bind(this)); 88 this._createAndRegisterPort(); 89 } 90 91 /** @type {import('offscreen').ApiHandler<'clipboardGetTextOffscreen'>} */ 92 async _getTextHandler({useRichText}) { 93 return await this._clipboardReader.getText(useRichText); 94 } 95 96 /** @type {import('offscreen').ApiHandler<'clipboardGetImageOffscreen'>} */ 97 async _getImageHandler() { 98 return await this._clipboardReader.getImage(); 99 } 100 101 /** @type {import('offscreen').ApiHandler<'clipboardSetBrowserOffscreen'>} */ 102 _setClipboardBrowser({value}) { 103 this._clipboardReader.browser = value; 104 } 105 106 /** @type {import('offscreen').ApiHandler<'databasePrepareOffscreen'>} */ 107 _prepareDatabaseHandler() { 108 if (this._prepareDatabasePromise !== null) { 109 return this._prepareDatabasePromise; 110 } 111 this._prepareDatabasePromise = this._dictionaryDatabase.prepare(); 112 return this._prepareDatabasePromise; 113 } 114 115 /** @type {import('offscreen').ApiHandler<'getDictionaryInfoOffscreen'>} */ 116 async _getDictionaryInfoHandler() { 117 return await this._dictionaryDatabase.getDictionaryInfo(); 118 } 119 120 /** @type {import('offscreen').ApiHandler<'databasePurgeOffscreen'>} */ 121 async _purgeDatabaseHandler() { 122 return await this._dictionaryDatabase.purge(); 123 } 124 125 /** @type {import('offscreen').ApiHandler<'databaseGetMediaOffscreen'>} */ 126 async _getMediaHandler({targets}) { 127 const media = await this._dictionaryDatabase.getMedia(targets); 128 return media.map((m) => ({...m, content: arrayBufferToBase64(m.content)})); 129 } 130 131 /** @type {import('offscreen').ApiHandler<'translatorPrepareOffscreen'>} */ 132 _prepareTranslatorHandler() { 133 this._translator.prepare(); 134 } 135 136 /** @type {import('offscreen').ApiHandler<'findKanjiOffscreen'>} */ 137 async _findKanjiHandler({text, options}) { 138 /** @type {import('translation').FindKanjiOptions} */ 139 const modifiedOptions = { 140 ...options, 141 enabledDictionaryMap: new Map(options.enabledDictionaryMap), 142 }; 143 return await this._translator.findKanji(text, modifiedOptions); 144 } 145 146 /** @type {import('offscreen').ApiHandler<'findTermsOffscreen'>} */ 147 async _findTermsHandler({mode, text, options}) { 148 const enabledDictionaryMap = new Map(options.enabledDictionaryMap); 149 const excludeDictionaryDefinitions = ( 150 options.excludeDictionaryDefinitions !== null ? 151 new Set(options.excludeDictionaryDefinitions) : 152 null 153 ); 154 const textReplacements = options.textReplacements.map((group) => { 155 if (group === null) { return null; } 156 return group.map((opt) => { 157 // https://stackoverflow.com/a/33642463 158 const match = opt.pattern.match(/\/(.*?)\/([a-z]*)?$/i); 159 const [, pattern, flags] = match !== null ? match : ['', '', '']; 160 return {...opt, pattern: new RegExp(pattern, flags ?? '')}; 161 }); 162 }); 163 /** @type {import('translation').FindTermsOptions} */ 164 const modifiedOptions = { 165 ...options, 166 enabledDictionaryMap, 167 excludeDictionaryDefinitions, 168 textReplacements, 169 }; 170 return this._translator.findTerms(mode, text, modifiedOptions); 171 } 172 173 /** @type {import('offscreen').ApiHandler<'getTermFrequenciesOffscreen'>} */ 174 _getTermFrequenciesHandler({termReadingList, dictionaries}) { 175 return this._translator.getTermFrequencies(termReadingList, dictionaries); 176 } 177 178 /** @type {import('offscreen').ApiHandler<'clearDatabaseCachesOffscreen'>} */ 179 _clearDatabaseCachesHandler() { 180 this._translator.clearDatabaseCaches(); 181 } 182 183 /** @type {import('extension').ChromeRuntimeOnMessageCallback<import('offscreen').ApiMessageAny>} */ 184 _onMessage({action, params}, _sender, callback) { 185 return invokeApiMapHandler(this._apiMap, action, params, [], callback); 186 } 187 188 /** 189 * 190 */ 191 _createAndRegisterPort() { 192 const mc = new MessageChannel(); 193 mc.port1.onmessage = this._onMcMessage.bind(this); 194 mc.port1.onmessageerror = this._onMcMessageError.bind(this); 195 this._api.registerOffscreenPort([mc.port2]); 196 } 197 198 /** @type {import('offscreen').McApiHandler<'connectToDatabaseWorker'>} */ 199 async _connectToDatabaseWorkerHandler(_params, ports) { 200 await this._dictionaryDatabase.connectToDatabaseWorker(ports[0]); 201 } 202 203 /** @type {import('offscreen').ApiHandler<'sanitizeCSSOffscreen'>} */ 204 _sanitizeCSSOffscreen(params) { 205 return sanitizeCSS(params.css); 206 } 207 208 /** 209 * @param {MessageEvent<import('offscreen').McApiMessageAny>} event 210 */ 211 _onMcMessage(event) { 212 const {action, params} = event.data; 213 invokeApiMapHandler(this._mcApiMap, action, params, [event.ports], () => {}); 214 } 215 216 /** 217 * @param {MessageEvent<import('offscreen').McApiMessageAny>} event 218 */ 219 _onMcMessageError(event) { 220 const error = new ExtensionError('Offscreen: Error receiving message via postMessage'); 221 error.data = event; 222 log.error(error); 223 } 224}