aboutsummaryrefslogtreecommitdiff
path: root/ext/js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js')
-rw-r--r--ext/js/background/offscreen-proxy.js137
-rw-r--r--ext/js/background/offscreen.js121
-rw-r--r--ext/js/language/translator.js4
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) {