diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2023-11-27 17:43:53 -0500 | 
|---|---|---|
| committer | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2023-11-27 17:43:53 -0500 | 
| commit | 29317da4ea237557e1805a834913dad73c51ed8a (patch) | |
| tree | e19ff6014774b42a95c22dbc7874b51d7bc3bf66 /ext/js | |
| parent | a96be7d413c8f1714f0d1b44b3987f2eb87b22d8 (diff) | |
Update offscreen
Diffstat (limited to 'ext/js')
| -rw-r--r-- | ext/js/background/offscreen-proxy.js | 137 | ||||
| -rw-r--r-- | ext/js/background/offscreen.js | 121 | ||||
| -rw-r--r-- | ext/js/language/translator.js | 4 | 
3 files changed, 170 insertions, 92 deletions
| diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js index 0fb2f269..757d78d5 100644 --- a/ext/js/background/offscreen-proxy.js +++ b/ext/js/background/offscreen-proxy.js @@ -18,13 +18,17 @@  import {isObject} from '../core.js';  import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; +import {ExtensionError} from '../core/extension-error.js';  export class OffscreenProxy {      constructor() { +        /** @type {?Promise<void>} */          this._creatingOffscreen = null;      } -    // https://developer.chrome.com/docs/extensions/reference/offscreen/ +    /** +     * @see https://developer.chrome.com/docs/extensions/reference/offscreen/ +     */      async prepare() {          if (await this._hasOffscreenDocument()) {              return; @@ -36,20 +40,30 @@ export class OffscreenProxy {          this._creatingOffscreen = chrome.offscreen.createDocument({              url: 'offscreen.html', -            reasons: ['CLIPBOARD'], +            reasons: [ +                /** @type {chrome.offscreen.Reason} */ ('CLIPBOARD') +            ],              justification: 'Access to the clipboard'          });          await this._creatingOffscreen;          this._creatingOffscreen = null;      } +    /** +     * @returns {Promise<boolean>} +     */      async _hasOffscreenDocument() {          const offscreenUrl = chrome.runtime.getURL('offscreen.html'); -        if (!chrome.runtime.getContexts) { // chrome version <116 +        // @ts-ignore - API not defined yet +        if (!chrome.runtime.getContexts) { // chrome version below 116 +            // Clients: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/clients +            // @ts-ignore - Types not set up for service workers yet              const matchedClients = await clients.matchAll(); +            // @ts-ignore - Types not set up for service workers yet              return await matchedClients.some((client) => client.url === offscreenUrl);          } +        // @ts-ignore - API not defined yet          const contexts = await chrome.runtime.getContexts({              contextTypes: ['OFFSCREEN_DOCUMENT'],              documentUrls: [offscreenUrl] @@ -57,103 +71,152 @@ export class OffscreenProxy {          return !!contexts.length;      } -    sendMessagePromise(...args) { +    /** +     * @template {import('offscreen').MessageType} TMessageType +     * @param {import('offscreen').Message<TMessageType>} message +     * @returns {Promise<import('offscreen').MessageReturn<TMessageType>>} +     */ +    sendMessagePromise(message) {          return new Promise((resolve, reject) => { -            const callback = (response) => { +            chrome.runtime.sendMessage(message, (response) => {                  try {                      resolve(this._getMessageResponseResult(response));                  } catch (error) {                      reject(error);                  } -            }; - -            chrome.runtime.sendMessage(...args, callback); +            });          });      } +    /** +     * @template [TReturn=unknown] +     * @param {import('core').Response<TReturn>} response +     * @returns {TReturn} +     * @throws {Error} +     */      _getMessageResponseResult(response) { -        let error = chrome.runtime.lastError; +        const error = chrome.runtime.lastError;          if (error) {              throw new Error(error.message);          }          if (!isObject(response)) {              throw new Error('Offscreen document did not respond');          } -        error = response.error; -        if (error) { -            throw deserializeError(error); +        const error2 = response.error; +        if (error2) { +            throw ExtensionError.deserialize(error2);          }          return response.result;      }  }  export class DictionaryDatabaseProxy { +    /** +     * @param {OffscreenProxy} offscreen +     */      constructor(offscreen) { +        /** @type {OffscreenProxy} */          this._offscreen = offscreen;      } -    prepare() { -        return this._offscreen.sendMessagePromise({action: 'databasePrepareOffscreen'}); +    /** +     * @returns {Promise<void>} +     */ +    async prepare() { +        await this._offscreen.sendMessagePromise({action: 'databasePrepareOffscreen'});      } -    getDictionaryInfo() { +    /** +     * @returns {Promise<import('dictionary-importer').Summary[]>} +     */ +    async getDictionaryInfo() {          return this._offscreen.sendMessagePromise({action: 'getDictionaryInfoOffscreen'});      } -    purge() { -        return this._offscreen.sendMessagePromise({action: 'databasePurgeOffscreen'}); +    /** +     * @returns {Promise<boolean>} +     */ +    async purge() { +        return await this._offscreen.sendMessagePromise({action: 'databasePurgeOffscreen'});      } +    /** +     * @param {import('dictionary-database').MediaRequest[]} targets +     * @returns {Promise<import('dictionary-database').Media[]>} +     */      async getMedia(targets) { -        const serializedMedia = await this._offscreen.sendMessagePromise({action: 'databaseGetMediaOffscreen', params: {targets}}); +        const serializedMedia = /** @type {import('dictionary-database').Media<string>[]} */ (await this._offscreen.sendMessagePromise({action: 'databaseGetMediaOffscreen', params: {targets}}));          const media = serializedMedia.map((m) => ({...m, content: ArrayBufferUtil.base64ToArrayBuffer(m.content)}));          return media;      }  }  export class TranslatorProxy { +    /** +     * @param {OffscreenProxy} offscreen +     */      constructor(offscreen) { +        /** @type {OffscreenProxy} */          this._offscreen = offscreen;      } -    prepare(deinflectionReasons) { -        return this._offscreen.sendMessagePromise({action: 'translatorPrepareOffscreen', params: {deinflectionReasons}}); +    /** +     * @param {import('deinflector').ReasonsRaw} deinflectionReasons +     */ +    async prepare(deinflectionReasons) { +        await this._offscreen.sendMessagePromise({action: 'translatorPrepareOffscreen', params: {deinflectionReasons}});      } -    async findKanji(text, findKanjiOptions) { -        const enabledDictionaryMapList = [...findKanjiOptions.enabledDictionaryMap]; -        const modifiedKanjiOptions = { -            ...findKanjiOptions, +    /** +     * @param {string} text +     * @param {import('translation').FindKanjiOptions} options +     * @returns {Promise<import('dictionary').KanjiDictionaryEntry[]>} +     */ +    async findKanji(text, options) { +        const enabledDictionaryMapList = [...options.enabledDictionaryMap]; +        /** @type {import('offscreen').FindKanjiOptionsOffscreen} */ +        const modifiedOptions = { +            ...options,              enabledDictionaryMap: enabledDictionaryMapList          }; -        return this._offscreen.sendMessagePromise({action: 'findKanjiOffscreen', params: {text, findKanjiOptions: modifiedKanjiOptions}}); +        return this._offscreen.sendMessagePromise({action: 'findKanjiOffscreen', params: {text, options: modifiedOptions}});      } -    async findTerms(mode, text, findTermsOptions) { -        const {enabledDictionaryMap, excludeDictionaryDefinitions, textReplacements} = findTermsOptions; +    /** +     * @param {import('translator').FindTermsMode} mode +     * @param {string} text +     * @param {import('translation').FindTermsOptions} options +     * @returns {Promise<import('translator').FindTermsResult>} +     */ +    async findTerms(mode, text, options) { +        const {enabledDictionaryMap, excludeDictionaryDefinitions, textReplacements} = options;          const enabledDictionaryMapList = [...enabledDictionaryMap];          const excludeDictionaryDefinitionsList = excludeDictionaryDefinitions ? [...excludeDictionaryDefinitions] : null;          const textReplacementsSerialized = textReplacements.map((group) => { -            if (!group) { -                return group; -            } -            return group.map((opt) => ({...opt, pattern: opt.pattern.toString()})); +            return group !== null ? group.map((opt) => ({...opt, pattern: opt.pattern.toString()})) : null;          }); -        const modifiedFindTermsOptions = { -            ...findTermsOptions, +        /** @type {import('offscreen').FindTermsOptionsOffscreen} */ +        const modifiedOptions = { +            ...options,              enabledDictionaryMap: enabledDictionaryMapList,              excludeDictionaryDefinitions: excludeDictionaryDefinitionsList,              textReplacements: textReplacementsSerialized          }; -        return this._offscreen.sendMessagePromise({action: 'findTermsOffscreen', params: {mode, text, findTermsOptions: modifiedFindTermsOptions}}); +        return this._offscreen.sendMessagePromise({action: 'findTermsOffscreen', params: {mode, text, options: modifiedOptions}});      } +    /** +     * @param {import('translator').TermReadingList} termReadingList +     * @param {string[]} dictionaries +     * @returns {Promise<import('translator').TermFrequencySimple[]>} +     */      async getTermFrequencies(termReadingList, dictionaries) {          return this._offscreen.sendMessagePromise({action: 'getTermFrequenciesOffscreen', params: {termReadingList, dictionaries}});      } -    clearDatabaseCaches() { -        return this._offscreen.sendMessagePromise({action: 'clearDatabaseCachesOffscreen'}); +    /** */ +    async clearDatabaseCaches() { +        await this._offscreen.sendMessagePromise({action: 'clearDatabaseCachesOffscreen'});      }  } @@ -173,7 +236,7 @@ export class ClipboardReaderProxy {      set browser(value) {          if (this._browser === value) { return; }          this._browser = value; -        this._offscreen.sendMessagePromise({action: 'clipboardSetBrowserOffsecreen', params: {value}}); +        this._offscreen.sendMessagePromise({action: 'clipboardSetBrowserOffscreen', params: {value}});      }      /** diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js index 6302aa84..1b68887b 100644 --- a/ext/js/background/offscreen.js +++ b/ext/js/background/offscreen.js @@ -23,7 +23,6 @@ import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js';  import {DictionaryDatabase} from '../language/dictionary-database.js';  import {JapaneseUtil} from '../language/sandbox/japanese-util.js';  import {Translator} from '../language/translator.js'; -import {yomitan} from '../yomitan.js';  /**   * This class controls the core logic of the extension, including API calls @@ -34,12 +33,16 @@ export class Offscreen {       * Creates a new instance.       */      constructor() { +        /** @type {JapaneseUtil} */          this._japaneseUtil = new JapaneseUtil(wanakana); +        /** @type {DictionaryDatabase} */          this._dictionaryDatabase = new DictionaryDatabase(); +        /** @type {Translator} */          this._translator = new Translator({              japaneseUtil: this._japaneseUtil,              database: this._dictionaryDatabase          }); +        /** @type {ClipboardReader} */          this._clipboardReader = new ClipboardReader({              // eslint-disable-next-line no-undef              document: (typeof document === 'object' && document !== null ? document : null), @@ -47,42 +50,45 @@ export class Offscreen {              richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'          }); +        /** @type {import('offscreen').MessageHandlerMap} */          this._messageHandlers = new Map([ -            ['clipboardGetTextOffscreen',    {async: true,  contentScript: true,  handler: this._getTextHandler.bind(this)}], -            ['clipboardGetImageOffscreen',   {async: true,  contentScript: true,  handler: this._getImageHandler.bind(this)}], -            ['clipboardSetBrowserOffsecreen', {async: false, contentScript: true, handler: this._setClipboardBrowser.bind(this)}], -            ['databasePrepareOffscreen',     {async: true,  contentScript: true,  handler: this._prepareDatabaseHandler.bind(this)}], -            ['getDictionaryInfoOffscreen',   {async: true,  contentScript: true,  handler: this._getDictionaryInfoHandler.bind(this)}], -            ['databasePurgeOffscreen',       {async: true,  contentScript: true,  handler: this._purgeDatabaseHandler.bind(this)}], -            ['databaseGetMediaOffscreen',    {async: true,  contentScript: true,  handler: this._getMediaHandler.bind(this)}], -            ['translatorPrepareOffscreen',   {async: false,  contentScript: true,  handler: this._prepareTranslatorHandler.bind(this)}], -            ['findKanjiOffscreen',           {async: true,  contentScript: true,  handler: this._findKanjiHandler.bind(this)}], -            ['findTermsOffscreen',           {async: true,  contentScript: true,  handler: this._findTermsHandler.bind(this)}], -            ['getTermFrequenciesOffscreen',  {async: true,  contentScript: true,  handler: this._getTermFrequenciesHandler.bind(this)}], -            ['clearDatabaseCachesOffscreen', {async: false,  contentScript: true,  handler: this._clearDatabaseCachesHandler.bind(this)}] +            ['clipboardGetTextOffscreen',    {async: true,  handler: this._getTextHandler.bind(this)}], +            ['clipboardGetImageOffscreen',   {async: true,  handler: this._getImageHandler.bind(this)}], +            ['clipboardSetBrowserOffscreen', {async: false, handler: this._setClipboardBrowser.bind(this)}], +            ['databasePrepareOffscreen',     {async: true,  handler: this._prepareDatabaseHandler.bind(this)}], +            ['getDictionaryInfoOffscreen',   {async: true,  handler: this._getDictionaryInfoHandler.bind(this)}], +            ['databasePurgeOffscreen',       {async: true,  handler: this._purgeDatabaseHandler.bind(this)}], +            ['databaseGetMediaOffscreen',    {async: true,  handler: this._getMediaHandler.bind(this)}], +            ['translatorPrepareOffscreen',   {async: false, handler: this._prepareTranslatorHandler.bind(this)}], +            ['findKanjiOffscreen',           {async: true,  handler: this._findKanjiHandler.bind(this)}], +            ['findTermsOffscreen',           {async: true,  handler: this._findTermsHandler.bind(this)}], +            ['getTermFrequenciesOffscreen',  {async: true,  handler: this._getTermFrequenciesHandler.bind(this)}], +            ['clearDatabaseCachesOffscreen', {async: false, handler: this._clearDatabaseCachesHandler.bind(this)}]          ]);          const onMessage = this._onMessage.bind(this);          chrome.runtime.onMessage.addListener(onMessage); +        /** @type {?Promise<void>} */          this._prepareDatabasePromise = null;      } -    _getTextHandler({useRichText}) { -        return this._clipboardReader.getText(useRichText); +    /** @type {import('offscreen').MessageHandler<'clipboardGetTextOffscreen', true>} */ +    async _getTextHandler({useRichText}) { +        return await this._clipboardReader.getText(useRichText);      } -    _getImageHandler() { -        return this._clipboardReader.getImage(); +    /** @type {import('offscreen').MessageHandler<'clipboardGetImageOffscreen', true>} */ +    async _getImageHandler() { +        return await this._clipboardReader.getImage();      } -    /** -     * @param {{value: import('environment').Browser}} details -     */ +    /** @type {import('offscreen').MessageHandler<'clipboardSetBrowserOffscreen', false>} */      _setClipboardBrowser({value}) {          this._clipboardReader.browser = value;      } +    /** @type {import('offscreen').MessageHandler<'databasePrepareOffscreen', true>} */      _prepareDatabaseHandler() {          if (this._prepareDatabasePromise !== null) {              return this._prepareDatabasePromise; @@ -91,70 +97,79 @@ export class Offscreen {          return this._prepareDatabasePromise;      } -    _getDictionaryInfoHandler() { -        return this._dictionaryDatabase.getDictionaryInfo(); +    /** @type {import('offscreen').MessageHandler<'getDictionaryInfoOffscreen', true>} */ +    async _getDictionaryInfoHandler() { +        return await this._dictionaryDatabase.getDictionaryInfo();      } -    _purgeDatabaseHandler() { -        return this._dictionaryDatabase.purge(); +    /** @type {import('offscreen').MessageHandler<'databasePurgeOffscreen', true>} */ +    async _purgeDatabaseHandler() { +        return await this._dictionaryDatabase.purge();      } +    /** @type {import('offscreen').MessageHandler<'databaseGetMediaOffscreen', true>} */      async _getMediaHandler({targets}) {          const media = await this._dictionaryDatabase.getMedia(targets);          const serializedMedia = media.map((m) => ({...m, content: ArrayBufferUtil.arrayBufferToBase64(m.content)}));          return serializedMedia;      } +    /** @type {import('offscreen').MessageHandler<'translatorPrepareOffscreen', false>} */      _prepareTranslatorHandler({deinflectionReasons}) { -        return this._translator.prepare(deinflectionReasons); +        this._translator.prepare(deinflectionReasons);      } -    _findKanjiHandler({text, findKanjiOptions}) { -        findKanjiOptions.enabledDictionaryMap = new Map(findKanjiOptions.enabledDictionaryMap); -        return this._translator.findKanji(text, findKanjiOptions); +    /** @type {import('offscreen').MessageHandler<'findKanjiOffscreen', true>} */ +    async _findKanjiHandler({text, options}) { +        /** @type {import('translation').FindKanjiOptions} */ +        const modifiedOptions = { +            ...options, +            enabledDictionaryMap: new Map(options.enabledDictionaryMap) +        }; +        return await this._translator.findKanji(text, modifiedOptions);      } -    _findTermsHandler({mode, text, findTermsOptions}) { -        findTermsOptions.enabledDictionaryMap = new Map(findTermsOptions.enabledDictionaryMap); -        if (findTermsOptions.excludeDictionaryDefinitions) { -            findTermsOptions.excludeDictionaryDefinitions = new Set(findTermsOptions.excludeDictionaryDefinitions); -        } -        findTermsOptions.textReplacements = findTermsOptions.textReplacements.map((group) => { -            if (!group) { -                return group; -            } +    /** @type {import('offscreen').MessageHandler<'findTermsOffscreen', true>} */ +    _findTermsHandler({mode, text, options}) { +        const enabledDictionaryMap = new Map(options.enabledDictionaryMap); +        const excludeDictionaryDefinitions = ( +            options.excludeDictionaryDefinitions !== null ? +            new Set(options.excludeDictionaryDefinitions) : +            null +        ); +        const textReplacements = options.textReplacements.map((group) => { +            if (group === null) { return null; }              return group.map((opt) => { -                const [, pattern, flags] = opt.pattern.match(/\/(.*?)\/([a-z]*)?$/i); // https://stackoverflow.com/a/33642463 +                // https://stackoverflow.com/a/33642463 +                const match = opt.pattern.match(/\/(.*?)\/([a-z]*)?$/i); +                const [, pattern, flags] = match !== null ? match : ['', '', ''];                  return {...opt, pattern: new RegExp(pattern, flags ?? '')};              });          }); -        return this._translator.findTerms(mode, text, findTermsOptions); +        /** @type {import('translation').FindTermsOptions} */ +        const modifiedOptions = { +            ...options, +            enabledDictionaryMap, +            excludeDictionaryDefinitions, +            textReplacements +        }; +        return this._translator.findTerms(mode, text, modifiedOptions);      } +    /** @type {import('offscreen').MessageHandler<'getTermFrequenciesOffscreen', true>} */      _getTermFrequenciesHandler({termReadingList, dictionaries}) {          return this._translator.getTermFrequencies(termReadingList, dictionaries);      } +    /** @type {import('offscreen').MessageHandler<'clearDatabaseCachesOffscreen', false>} */      _clearDatabaseCachesHandler() { -        return this._translator.clearDatabaseCaches(); +        this._translator.clearDatabaseCaches();      } +    /** @type {import('extension').ChromeRuntimeOnMessageCallback} */      _onMessage({action, params}, sender, callback) {          const messageHandler = this._messageHandlers.get(action);          if (typeof messageHandler === 'undefined') { return false; } -        this._validatePrivilegedMessageSender(sender); -          return invokeMessageHandler(messageHandler, params, callback, sender);      } - -    _validatePrivilegedMessageSender(sender) { -        let {url} = sender; -        if (typeof url === 'string' && yomitan.isExtensionUrl(url)) { return; } -        const {tab} = url; -        if (typeof tab === 'object' && tab !== null) { -            ({url} = tab); -            if (typeof url === 'string' && yomitan.isExtensionUrl(url)) { return; } -        } -        throw new Error('Invalid message sender'); -    }  } diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index c21b16b1..aa1b71dd 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -157,7 +157,7 @@ export class Translator {      /**       * Gets a list of frequency information for a given list of term-reading pairs       * and a list of dictionaries. -     * @param {{term: string, reading: string|null}[]} termReadingList An array of `{term, reading}` pairs. If reading is null, +     * @param {import('translator').TermReadingList} termReadingList An array of `{term, reading}` pairs. If reading is null,       *   the reading won't be compared.       * @param {string[]} dictionaries An array of dictionary names.       * @returns {Promise<import('translator').TermFrequencySimple[]>} An array of term frequencies. @@ -203,7 +203,7 @@ export class Translator {       * @param {Map<string, import('translation').FindTermDictionary>} enabledDictionaryMap       * @param {import('translation').FindTermsOptions} options       * @param {TranslatorTagAggregator} tagAggregator -     * @returns {Promise<{dictionaryEntries: import('dictionary').TermDictionaryEntry[], originalTextLength: number}>} +     * @returns {Promise<import('translator').FindTermsResult>}       */      async _findTermsInternal(text, enabledDictionaryMap, options, tagAggregator) {          if (options.removeNonJapaneseCharacters) { |