From 4da4827bcbcdd1ef163f635d9b29416ff272b0bb Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Nov 2023 12:48:14 -0500 Subject: Add JSDoc type annotations to project (rebased) --- ext/js/comm/anki-connect.js | 310 +++++++++++++++++++++++++---- ext/js/comm/api.js | 356 +++++++++++++++++++++++++++++----- ext/js/comm/clipboard-monitor.js | 24 ++- ext/js/comm/clipboard-reader.js | 54 ++++-- ext/js/comm/cross-frame-api.js | 156 +++++++++++++-- ext/js/comm/frame-ancestry-handler.js | 87 +++++++-- ext/js/comm/frame-client.js | 68 +++++-- ext/js/comm/frame-endpoint.js | 49 +++-- ext/js/comm/frame-offset-forwarder.js | 16 ++ ext/js/comm/mecab.js | 84 +++++--- 10 files changed, 1025 insertions(+), 179 deletions(-) (limited to 'ext/js/comm') diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index 09838ea5..3262af41 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -27,17 +27,23 @@ export class AnkiConnect { * Creates a new instance. */ constructor() { + /** @type {boolean} */ this._enabled = false; + /** @type {?string} */ this._server = null; + /** @type {number} */ this._localVersion = 2; + /** @type {number} */ this._remoteVersion = 0; + /** @type {?Promise} */ this._versionCheckPromise = null; + /** @type {?string} */ this._apiKey = null; } /** * Gets the URL of the AnkiConnect server. - * @type {string} + * @type {?string} */ get server() { return this._server; @@ -90,7 +96,7 @@ export class AnkiConnect { */ async isConnected() { try { - await this._invoke('version'); + await this._getVersion(); return true; } catch (e) { return false; @@ -99,74 +105,114 @@ export class AnkiConnect { /** * Gets the AnkiConnect API version number. - * @returns {Promise} The version number + * @returns {Promise} The version number */ async getVersion() { if (!this._enabled) { return null; } await this._checkVersion(); - return await this._invoke('version', {}); + return await this._getVersion(); } + /** + * @param {import('anki').Note} note + * @returns {Promise} + */ async addNote(note) { if (!this._enabled) { return null; } await this._checkVersion(); - return await this._invoke('addNote', {note}); + const result = await this._invoke('addNote', {note}); + if (result !== null && typeof result !== 'number') { + throw this._createUnexpectedResultError('number|null', result); + } + return result; } + /** + * @param {import('anki').Note[]} notes + * @returns {Promise} + */ async canAddNotes(notes) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._invoke('canAddNotes', {notes}); + const result = await this._invoke('canAddNotes', {notes}); + return this._normalizeArray(result, notes.length, 'boolean'); } - async notesInfo(notes) { + /** + * @param {import('anki').NoteId[]} noteIds + * @returns {Promise<(?import('anki').NoteInfo)[]>} + */ + async notesInfo(noteIds) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._invoke('notesInfo', {notes}); + const result = await this._invoke('notesInfo', {notes: noteIds}); + return this._normalizeNoteInfoArray(result); } + /** + * @returns {Promise} + */ async getDeckNames() { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._invoke('deckNames'); + const result = await this._invoke('deckNames', {}); + return this._normalizeArray(result, -1, 'string'); } + /** + * @returns {Promise} + */ async getModelNames() { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._invoke('modelNames'); + const result = await this._invoke('modelNames', {}); + return this._normalizeArray(result, -1, 'string'); } + /** + * @param {string} modelName + * @returns {Promise} + */ async getModelFieldNames(modelName) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._invoke('modelFieldNames', {modelName}); + const result = await this._invoke('modelFieldNames', {modelName}); + return this._normalizeArray(result, -1, 'string'); } + /** + * @param {string} query + * @returns {Promise} + */ async guiBrowse(query) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._invoke('guiBrowse', {query}); + const result = await this._invoke('guiBrowse', {query}); + return this._normalizeArray(result, -1, 'number'); } + /** + * @param {import('anki').NoteId} noteId + * @returns {Promise} + */ async guiBrowseNote(noteId) { return await this.guiBrowse(`nid:${noteId}`); } /** * Opens the note editor GUI. - * @param {number} noteId The ID of the note. - * @returns {Promise} Nothing is returned. + * @param {import('anki').NoteId} noteId The ID of the note. + * @returns {Promise} Nothing is returned. */ async guiEditNote(noteId) { - return await this._invoke('guiEditNote', {note: noteId}); + await this._invoke('guiEditNote', {note: noteId}); } /** * Stores a file with the specified base64-encoded content inside Anki's media folder. * @param {string} fileName The name of the file. * @param {string} content The base64-encoded content of the file. - * @returns {?string} The actual file name used to store the file, which may be different; or `null` if the file was not stored. + * @returns {Promise} The actual file name used to store the file, which may be different; or `null` if the file was not stored. * @throws {Error} An error is thrown is this object is not enabled. */ async storeMediaFile(fileName, content) { @@ -174,28 +220,39 @@ export class AnkiConnect { throw new Error('AnkiConnect not enabled'); } await this._checkVersion(); - return await this._invoke('storeMediaFile', {filename: fileName, data: content}); + const result = await this._invoke('storeMediaFile', {filename: fileName, data: content}); + if (result !== null && typeof result !== 'string') { + throw this._createUnexpectedResultError('string|null', result); + } + return result; } /** * Finds notes matching a query. * @param {string} query Searches for notes matching a query. - * @returns {number[]} An array of note IDs. + * @returns {Promise} An array of note IDs. * @see https://docs.ankiweb.net/searching.html */ async findNotes(query) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._invoke('findNotes', {query}); + const result = await this._invoke('findNotes', {query}); + return this._normalizeArray(result, -1, 'number'); } + /** + * @param {import('anki').Note[]} notes + * @returns {Promise} + */ async findNoteIds(notes) { if (!this._enabled) { return []; } await this._checkVersion(); const actions = []; const actionsTargetsList = []; + /** @type {Map} */ const actionsTargetsMap = new Map(); + /** @type {import('anki').NoteId[][]} */ const allNoteIds = []; for (const note of notes) { @@ -207,14 +264,15 @@ export class AnkiConnect { actionsTargetsMap.set(query, actionsTargets); actions.push({action: 'findNotes', params: {query}}); } + /** @type {import('anki').NoteId[]} */ const noteIds = []; allNoteIds.push(noteIds); actionsTargets.push(noteIds); } - const result = await this._invoke('multi', {actions}); + const result = await this._invokeMulti(actions); for (let i = 0, ii = Math.min(result.length, actionsTargetsList.length); i < ii; ++i) { - const noteIds = result[i]; + const noteIds = /** @type {number[]} */ (this._normalizeArray(result[i], -1, 'number')); for (const actionsTargets of actionsTargetsList[i]) { for (const noteId of noteIds) { actionsTargets.push(noteId); @@ -224,18 +282,32 @@ export class AnkiConnect { return allNoteIds; } + /** + * @param {import('anki').CardId[]} cardIds + * @returns {Promise} + */ async suspendCards(cardIds) { if (!this._enabled) { return false; } await this._checkVersion(); - return await this._invoke('suspend', {cards: cardIds}); + const result = await this._invoke('suspend', {cards: cardIds}); + return typeof result === 'boolean' && result; } + /** + * @param {string} query + * @returns {Promise} + */ async findCards(query) { if (!this._enabled) { return []; } await this._checkVersion(); - return await this._invoke('findCards', {query}); + const result = await this._invoke('findCards', {query}); + return this._normalizeArray(result, -1, 'number'); } + /** + * @param {import('anki').NoteId} noteId + * @returns {Promise} + */ async findCardsForNote(noteId) { return await this.findCards(`nid:${noteId}`); } @@ -244,16 +316,26 @@ export class AnkiConnect { * Gets information about the AnkiConnect APIs available. * @param {string[]} scopes A list of scopes to get information about. * @param {?string[]} actions A list of actions to check for - * @returns {object} Information about the APIs. + * @returns {Promise} Information about the APIs. */ async apiReflect(scopes, actions=null) { - return await this._invoke('apiReflect', {scopes, actions}); + const result = await this._invoke('apiReflect', {scopes, actions}); + if (!(typeof result === 'object' && result !== null)) { + throw this._createUnexpectedResultError('object', result); + } + const {scopes: resultScopes, actions: resultActions} = /** @type {import('core').SerializableObject} */ (result); + const resultScopes2 = /** @type {string[]} */ (this._normalizeArray(resultScopes, -1, 'string', ', field scopes')); + const resultActions2 = /** @type {string[]} */ (this._normalizeArray(resultActions, -1, 'string', ', field scopes')); + return { + scopes: resultScopes2, + actions: resultActions2 + }; } /** * Checks whether a specific API action exists. * @param {string} action The action to check for. - * @returns {boolean} Whether or not the action exists. + * @returns {Promise} Whether or not the action exists. */ async apiExists(action) { const {actions} = await this.apiReflect(['actions'], [action]); @@ -266,9 +348,9 @@ export class AnkiConnect { * @returns {boolean} Whether or not the error indicates the action is not supported. */ isErrorUnsupportedAction(error) { - if (error instanceof Error) { + if (error instanceof ExtensionError) { const {data} = error; - if (isObject(data) && data.apiError === 'unsupported action') { + if (typeof data === 'object' && data !== null && /** @type {import('core').SerializableObject} */ (data).apiError === 'unsupported action') { return true; } } @@ -277,10 +359,13 @@ export class AnkiConnect { // Private + /** + * @returns {Promise} + */ async _checkVersion() { if (this._remoteVersion < this._localVersion) { if (this._versionCheckPromise === null) { - const promise = this._invoke('version'); + const promise = this._getVersion(); promise .catch(() => {}) .finally(() => { this._versionCheckPromise = null; }); @@ -293,11 +378,18 @@ export class AnkiConnect { } } + /** + * @param {string} action + * @param {import('core').SerializableObject} params + * @returns {Promise} + */ async _invoke(action, params) { + /** @type {import('anki').MessageBody} */ const body = {action, params, version: this._localVersion}; if (this._apiKey !== null) { body.key = this._apiKey; } let response; try { + if (this._server === null) { throw new Error('Server URL is null'); } response = await fetch(this._server, { method: 'POST', mode: 'cors', @@ -311,33 +403,34 @@ export class AnkiConnect { body: JSON.stringify(body) }); } catch (e) { - const error = new Error('Anki connection failure'); + const error = new ExtensionError('Anki connection failure'); error.data = {action, params, originalError: e}; throw error; } if (!response.ok) { - const error = new Error(`Anki connection error: ${response.status}`); + const error = new ExtensionError(`Anki connection error: ${response.status}`); error.data = {action, params, status: response.status}; throw error; } let responseText = null; + /** @type {unknown} */ let result; try { responseText = await response.text(); result = JSON.parse(responseText); } catch (e) { - const error = new Error('Invalid Anki response'); + const error = new ExtensionError('Invalid Anki response'); error.data = {action, params, status: response.status, responseText, originalError: e}; throw error; } - if (isObject(result)) { - const apiError = result.error; + if (typeof result === 'object' && result !== null && !Array.isArray(result)) { + const apiError = /** @type {import('core').SerializableObject} */ (result).error; if (typeof apiError !== 'undefined') { - const error = new Error(`Anki error: ${apiError}`); - error.data = {action, params, status: response.status, apiError}; + const error = new ExtensionError(`Anki error: ${apiError}`); + error.data = {action, params, status: response.status, apiError: typeof apiError === 'string' ? apiError : `${apiError}`}; throw error; } } @@ -345,10 +438,30 @@ export class AnkiConnect { return result; } + /** + * @param {{action: string, params: import('core').SerializableObject}[]} actions + * @returns {Promise} + */ + async _invokeMulti(actions) { + const result = await this._invoke('multi', {actions}); + if (!Array.isArray(result)) { + throw this._createUnexpectedResultError('array', result); + } + return result; + } + + /** + * @param {string} text + * @returns {string} + */ _escapeQuery(text) { return text.replace(/"/g, ''); } + /** + * @param {import('anki').NoteFields} fields + * @returns {string} + */ _fieldsToQuery(fields) { const fieldNames = Object.keys(fields); if (fieldNames.length === 0) { @@ -359,6 +472,10 @@ export class AnkiConnect { return `"${key.toLowerCase()}:${this._escapeQuery(fields[key])}"`; } + /** + * @param {import('anki').Note} note + * @returns {?('collection'|'deck'|'deck-root')} + */ _getDuplicateScopeFromNote(note) { const {options} = note; if (typeof options === 'object' && options !== null) { @@ -370,6 +487,10 @@ export class AnkiConnect { return null; } + /** + * @param {import('anki').Note} note + * @returns {string} + */ _getNoteQuery(note) { let query = ''; switch (this._getDuplicateScopeFromNote(note)) { @@ -383,4 +504,121 @@ export class AnkiConnect { query += this._fieldsToQuery(note.fields); return query; } + + /** + * @returns {Promise} + */ + async _getVersion() { + const version = await this._invoke('version', {}); + return typeof version === 'number' ? version : 0; + } + + /** + * @param {string} message + * @param {unknown} data + * @returns {ExtensionError} + */ + _createError(message, data) { + const error = new ExtensionError(message); + error.data = data; + return error; + } + + /** + * @param {string} expectedType + * @param {unknown} result + * @param {string} [context] + * @returns {ExtensionError} + */ + _createUnexpectedResultError(expectedType, result, context) { + return this._createError(`Unexpected type${typeof context === 'string' ? context : ''}: expected ${expectedType}, received ${this._getTypeName(result)}`, result); + } + + /** + * @param {unknown} value + * @returns {string} + */ + _getTypeName(value) { + if (value === null) { return 'null'; } + return Array.isArray(value) ? 'array' : typeof value; + } + + /** + * @template [T=unknown] + * @param {unknown} result + * @param {number} expectedCount + * @param {'boolean'|'string'|'number'} type + * @param {string} [context] + * @returns {T[]} + * @throws {Error} + */ + _normalizeArray(result, expectedCount, type, context) { + if (!Array.isArray(result)) { + throw this._createUnexpectedResultError(`${type}[]`, result, context); + } + if (expectedCount < 0) { + expectedCount = result.length; + } else if (expectedCount !== result.length) { + throw this._createError(`Unexpected result array size${context}: expected ${expectedCount}, received ${result.length}`, result); + } + for (let i = 0; i < expectedCount; ++i) { + const item = /** @type {unknown} */ (result[i]); + if (typeof item !== type) { + throw this._createError(`Unexpected result type at index ${i}${context}: expected ${type}, received ${this._getTypeName(item)}`, result); + } + } + return /** @type {T[]} */ (result); + } + + /** + * @param {unknown} result + * @returns {(?import('anki').NoteInfo)[]} + * @throws {Error} + */ + _normalizeNoteInfoArray(result) { + if (!Array.isArray(result)) { + throw this._createUnexpectedResultError('array', result, ''); + } + /** @type {(?import('anki').NoteInfo)[]} */ + const result2 = []; + for (let i = 0, ii = result.length; i < ii; ++i) { + const item = /** @type {unknown} */ (result[i]); + if (item === null || typeof item !== 'object') { + throw this._createError(`Unexpected result type at index ${i}: expected Notes.NoteInfo, received ${this._getTypeName(item)}`, result); + } + const {noteId} = /** @type {{[key: string]: unknown}} */ (item); + if (typeof noteId !== 'number') { + result2.push(null); + continue; + } + + const {tags, fields, modelName, cards} = /** @type {{[key: string]: unknown}} */ (item); + if (typeof modelName !== 'string') { + throw this._createError(`Unexpected result type at index ${i}, field modelName: expected string, received ${this._getTypeName(modelName)}`, result); + } + if (typeof fields !== 'object' || fields === null) { + throw this._createError(`Unexpected result type at index ${i}, field fields: expected string, received ${this._getTypeName(fields)}`, result); + } + const tags2 = /** @type {string[]} */ (this._normalizeArray(tags, -1, 'string', ', field tags')); + const cards2 = /** @type {number[]} */ (this._normalizeArray(cards, -1, 'number', ', field cards')); + /** @type {{[key: string]: import('anki').NoteFieldInfo}} */ + const fields2 = {}; + for (const [key, fieldInfo] of Object.entries(fields)) { + if (typeof fieldInfo !== 'object' || fieldInfo === null) { continue; } + const {value, order} = fieldInfo; + if (typeof value !== 'string' || typeof order !== 'number') { continue; } + fields2[key] = {value, order}; + } + /** @type {import('anki').NoteInfo} */ + const item2 = { + noteId, + tags: tags2, + fields: fields2, + modelName, + cards: cards2 + }; + result2.push(item2); + } + return result2; + } } diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 05f95464..62dc98b1 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -16,184 +16,428 @@ * along with this program. If not, see . */ -import {deferPromise, deserializeError, isObject} from '../core.js'; +import {deferPromise} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js'; export class API { + /** + * @param {import('../yomitan.js').Yomitan} yomitan + */ constructor(yomitan) { + /** @type {import('../yomitan.js').Yomitan} */ this._yomitan = yomitan; } + /** + * @param {import('api').OptionsGetDetails['optionsContext']} optionsContext + * @returns {Promise} + */ optionsGet(optionsContext) { - return this._invoke('optionsGet', {optionsContext}); + /** @type {import('api').OptionsGetDetails} */ + const details = {optionsContext}; + return this._invoke('optionsGet', details); } + /** + * @returns {Promise} + */ optionsGetFull() { return this._invoke('optionsGetFull'); } + /** + * @param {import('api').TermsFindDetails['text']} text + * @param {import('api').TermsFindDetails['details']} details + * @param {import('api').TermsFindDetails['optionsContext']} optionsContext + * @returns {Promise} + */ termsFind(text, details, optionsContext) { - return this._invoke('termsFind', {text, details, optionsContext}); - } - + /** @type {import('api').TermsFindDetails} */ + const details2 = {text, details, optionsContext}; + return this._invoke('termsFind', details2); + } + + /** + * @param {import('api').ParseTextDetails['text']} text + * @param {import('api').ParseTextDetails['optionsContext']} optionsContext + * @param {import('api').ParseTextDetails['scanLength']} scanLength + * @param {import('api').ParseTextDetails['useInternalParser']} useInternalParser + * @param {import('api').ParseTextDetails['useMecabParser']} useMecabParser + * @returns {Promise} + */ parseText(text, optionsContext, scanLength, useInternalParser, useMecabParser) { - return this._invoke('parseText', {text, optionsContext, scanLength, useInternalParser, useMecabParser}); + /** @type {import('api').ParseTextDetails} */ + const details = {text, optionsContext, scanLength, useInternalParser, useMecabParser}; + return this._invoke('parseText', details); } + /** + * @param {import('api').KanjiFindDetails['text']} text + * @param {import('api').KanjiFindDetails['optionsContext']} optionsContext + * @returns {Promise} + */ kanjiFind(text, optionsContext) { - return this._invoke('kanjiFind', {text, optionsContext}); + /** @type {import('api').KanjiFindDetails} */ + const details = {text, optionsContext}; + return this._invoke('kanjiFind', details); } + /** + * @returns {Promise} + */ isAnkiConnected() { return this._invoke('isAnkiConnected'); } + /** + * @returns {Promise} + */ getAnkiConnectVersion() { return this._invoke('getAnkiConnectVersion'); } + /** + * @param {import('api').AddAnkiNoteDetails['note']} note + * @returns {Promise} + */ addAnkiNote(note) { - return this._invoke('addAnkiNote', {note}); + /** @type {import('api').AddAnkiNoteDetails} */ + const details = {note}; + return this._invoke('addAnkiNote', details); } + /** + * @param {import('api').GetAnkiNoteInfoDetails['notes']} notes + * @param {import('api').GetAnkiNoteInfoDetails['fetchAdditionalInfo']} fetchAdditionalInfo + * @returns {Promise} + */ getAnkiNoteInfo(notes, fetchAdditionalInfo) { - return this._invoke('getAnkiNoteInfo', {notes, fetchAdditionalInfo}); - } - + /** @type {import('api').GetAnkiNoteInfoDetails} */ + const details = {notes, fetchAdditionalInfo}; + return this._invoke('getAnkiNoteInfo', details); + } + + /** + * @param {import('api').InjectAnkiNoteMediaDetails['timestamp']} timestamp + * @param {import('api').InjectAnkiNoteMediaDetails['definitionDetails']} definitionDetails + * @param {import('api').InjectAnkiNoteMediaDetails['audioDetails']} audioDetails + * @param {import('api').InjectAnkiNoteMediaDetails['screenshotDetails']} screenshotDetails + * @param {import('api').InjectAnkiNoteMediaDetails['clipboardDetails']} clipboardDetails + * @param {import('api').InjectAnkiNoteMediaDetails['dictionaryMediaDetails']} dictionaryMediaDetails + * @returns {Promise} + */ injectAnkiNoteMedia(timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails) { - return this._invoke('injectAnkiNoteMedia', {timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}); + /** @type {import('api').InjectAnkiNoteMediaDetails} */ + const details = {timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}; + return this._invoke('injectAnkiNoteMedia', details); } + /** + * @param {import('api').NoteViewDetails['noteId']} noteId + * @param {import('api').NoteViewDetails['mode']} mode + * @param {import('api').NoteViewDetails['allowFallback']} allowFallback + * @returns {Promise} + */ noteView(noteId, mode, allowFallback) { - return this._invoke('noteView', {noteId, mode, allowFallback}); + /** @type {import('api').NoteViewDetails} */ + const details = {noteId, mode, allowFallback}; + return this._invoke('noteView', details); } + /** + * @param {import('api').SuspendAnkiCardsForNoteDetails['noteId']} noteId + * @returns {Promise} + */ suspendAnkiCardsForNote(noteId) { - return this._invoke('suspendAnkiCardsForNote', {noteId}); + /** @type {import('api').SuspendAnkiCardsForNoteDetails} */ + const details = {noteId}; + return this._invoke('suspendAnkiCardsForNote', details); } + /** + * @param {import('api').GetTermAudioInfoListDetails['source']} source + * @param {import('api').GetTermAudioInfoListDetails['term']} term + * @param {import('api').GetTermAudioInfoListDetails['reading']} reading + * @returns {Promise} + */ getTermAudioInfoList(source, term, reading) { - return this._invoke('getTermAudioInfoList', {source, term, reading}); + /** @type {import('api').GetTermAudioInfoListDetails} */ + const details = {source, term, reading}; + return this._invoke('getTermAudioInfoList', details); } + /** + * @param {import('api').CommandExecDetails['command']} command + * @param {import('api').CommandExecDetails['params']} [params] + * @returns {Promise} + */ commandExec(command, params) { - return this._invoke('commandExec', {command, params}); + /** @type {import('api').CommandExecDetails} */ + const details = {command, params}; + return this._invoke('commandExec', details); } + /** + * @param {import('api').SendMessageToFrameDetails['frameId']} frameId + * @param {import('api').SendMessageToFrameDetails['action']} action + * @param {import('api').SendMessageToFrameDetails['params']} [params] + * @returns {Promise} + */ sendMessageToFrame(frameId, action, params) { - return this._invoke('sendMessageToFrame', {frameId, action, params}); + /** @type {import('api').SendMessageToFrameDetails} */ + const details = {frameId, action, params}; + return this._invoke('sendMessageToFrame', details); } + /** + * @param {import('api').BroadcastTabDetails['action']} action + * @param {import('api').BroadcastTabDetails['params']} params + * @returns {Promise} + */ broadcastTab(action, params) { - return this._invoke('broadcastTab', {action, params}); + /** @type {import('api').BroadcastTabDetails} */ + const details = {action, params}; + return this._invoke('broadcastTab', details); } + /** + * @returns {Promise} + */ frameInformationGet() { return this._invoke('frameInformationGet'); } + /** + * @param {import('api').InjectStylesheetDetails['type']} type + * @param {import('api').InjectStylesheetDetails['value']} value + * @returns {Promise} + */ injectStylesheet(type, value) { - return this._invoke('injectStylesheet', {type, value}); + /** @type {import('api').InjectStylesheetDetails} */ + const details = {type, value}; + return this._invoke('injectStylesheet', details); } + /** + * @param {import('api').GetStylesheetContentDetails['url']} url + * @returns {Promise} + */ getStylesheetContent(url) { - return this._invoke('getStylesheetContent', {url}); + /** @type {import('api').GetStylesheetContentDetails} */ + const details = {url}; + return this._invoke('getStylesheetContent', details); } + /** + * @returns {Promise} + */ getEnvironmentInfo() { return this._invoke('getEnvironmentInfo'); } + /** + * @returns {Promise} + */ clipboardGet() { return this._invoke('clipboardGet'); } + /** + * @returns {Promise} + */ getDisplayTemplatesHtml() { return this._invoke('getDisplayTemplatesHtml'); } + /** + * @returns {Promise} + */ getZoom() { return this._invoke('getZoom'); } + /** + * @returns {Promise} + */ getDefaultAnkiFieldTemplates() { return this._invoke('getDefaultAnkiFieldTemplates'); } + /** + * @returns {Promise} + */ getDictionaryInfo() { return this._invoke('getDictionaryInfo'); } + /** + * @returns {Promise} + */ purgeDatabase() { return this._invoke('purgeDatabase'); } + /** + * @param {import('api').GetMediaDetails['targets']} targets + * @returns {Promise} + */ getMedia(targets) { - return this._invoke('getMedia', {targets}); + /** @type {import('api').GetMediaDetails} */ + const details = {targets}; + return this._invoke('getMedia', details); } + /** + * @param {import('api').LogDetails['error']} error + * @param {import('api').LogDetails['level']} level + * @param {import('api').LogDetails['context']} context + * @returns {Promise} + */ log(error, level, context) { - return this._invoke('log', {error, level, context}); + /** @type {import('api').LogDetails} */ + const details = {error, level, context}; + return this._invoke('log', details); } + /** + * @returns {Promise} + */ logIndicatorClear() { return this._invoke('logIndicatorClear'); } + /** + * @param {import('api').ModifySettingsDetails['targets']} targets + * @param {import('api').ModifySettingsDetails['source']} source + * @returns {Promise} + */ modifySettings(targets, source) { - return this._invoke('modifySettings', {targets, source}); + const details = {targets, source}; + return this._invoke('modifySettings', details); } + /** + * @param {import('api').GetSettingsDetails['targets']} targets + * @returns {Promise} + */ getSettings(targets) { - return this._invoke('getSettings', {targets}); + /** @type {import('api').GetSettingsDetails} */ + const details = {targets}; + return this._invoke('getSettings', details); } + /** + * @param {import('api').SetAllSettingsDetails['value']} value + * @param {import('api').SetAllSettingsDetails['source']} source + * @returns {Promise} + */ setAllSettings(value, source) { - return this._invoke('setAllSettings', {value, source}); + /** @type {import('api').SetAllSettingsDetails} */ + const details = {value, source}; + return this._invoke('setAllSettings', details); } + /** + * @param {import('api').GetOrCreateSearchPopupDetails} details + * @returns {Promise} + */ getOrCreateSearchPopup(details) { - return this._invoke('getOrCreateSearchPopup', isObject(details) ? details : {}); + return this._invoke('getOrCreateSearchPopup', details); } + /** + * @param {import('api').IsTabSearchPopupDetails['tabId']} tabId + * @returns {Promise} + */ isTabSearchPopup(tabId) { - return this._invoke('isTabSearchPopup', {tabId}); + /** @type {import('api').IsTabSearchPopupDetails} */ + const details = {tabId}; + return this._invoke('isTabSearchPopup', details); } + /** + * @param {import('api').TriggerDatabaseUpdatedDetails['type']} type + * @param {import('api').TriggerDatabaseUpdatedDetails['cause']} cause + * @returns {Promise} + */ triggerDatabaseUpdated(type, cause) { - return this._invoke('triggerDatabaseUpdated', {type, cause}); + /** @type {import('api').TriggerDatabaseUpdatedDetails} */ + const details = {type, cause}; + return this._invoke('triggerDatabaseUpdated', details); } + /** + * @returns {Promise} + */ testMecab() { - return this._invoke('testMecab', {}); + return this._invoke('testMecab'); } + /** + * @param {import('api').TextHasJapaneseCharactersDetails['text']} text + * @returns {Promise} + */ textHasJapaneseCharacters(text) { - return this._invoke('textHasJapaneseCharacters', {text}); + /** @type {import('api').TextHasJapaneseCharactersDetails} */ + const details = {text}; + return this._invoke('textHasJapaneseCharacters', details); } + /** + * @param {import('api').GetTermFrequenciesDetails['termReadingList']} termReadingList + * @param {import('api').GetTermFrequenciesDetails['dictionaries']} dictionaries + * @returns {Promise} + */ getTermFrequencies(termReadingList, dictionaries) { - return this._invoke('getTermFrequencies', {termReadingList, dictionaries}); + /** @type {import('api').GetTermFrequenciesDetails} */ + const details = {termReadingList, dictionaries}; + return this._invoke('getTermFrequencies', details); } + /** + * @param {import('api').FindAnkiNotesDetails['query']} query + * @returns {Promise} + */ findAnkiNotes(query) { - return this._invoke('findAnkiNotes', {query}); + /** @type {import('api').FindAnkiNotesDetails} */ + const details = {query}; + return this._invoke('findAnkiNotes', details); } + /** + * @param {import('api').LoadExtensionScriptsDetails['files']} files + * @returns {Promise} + */ loadExtensionScripts(files) { - return this._invoke('loadExtensionScripts', {files}); + /** @type {import('api').LoadExtensionScriptsDetails} */ + const details = {files}; + return this._invoke('loadExtensionScripts', details); } + /** + * + * @param targetTabId + * @param targetFrameId + */ openCrossFramePort(targetTabId, targetFrameId) { return this._invoke('openCrossFramePort', {targetTabId, targetFrameId}); } // Utilities - _createActionPort(timeout=5000) { + /** + * @param {number} timeout + * @returns {Promise} + */ + _createActionPort(timeout) { return new Promise((resolve, reject) => { + /** @type {?number} */ let timer = null; const portDetails = deferPromise(); + /** + * @param {chrome.runtime.Port} port + */ const onConnect = async (port) => { try { const {name: expectedName, id: expectedId} = await portDetails.promise; @@ -210,6 +454,9 @@ export class API { resolve(port); }; + /** + * @param {Error} e + */ const onError = (e) => { if (timer !== null) { clearTimeout(timer); @@ -227,14 +474,24 @@ export class API { }); } - _invokeWithProgress(action, params, onProgress, timeout=5000) { + /** + * @template [TReturn=unknown] + * @param {string} action + * @param {import('core').SerializableObject} params + * @param {?(...args: unknown[]) => void} onProgress0 + * @param {number} [timeout] + * @returns {Promise} + */ + _invokeWithProgress(action, params, onProgress0, timeout=5000) { return new Promise((resolve, reject) => { + /** @type {?chrome.runtime.Port} */ let port = null; - if (typeof onProgress !== 'function') { - onProgress = () => {}; - } + const onProgress = typeof onProgress0 === 'function' ? onProgress0 : () => {}; + /** + * @param {import('backend').InvokeWithProgressResponseMessage} message + */ const onMessage = (message) => { switch (message.type) { case 'progress': @@ -250,7 +507,7 @@ export class API { break; case 'error': cleanup(); - reject(deserializeError(message.data)); + reject(ExtensionError.deserialize(message.data)); break; } }; @@ -267,7 +524,6 @@ export class API { port.disconnect(); port = null; } - onProgress = null; }; (async () => { @@ -281,20 +537,23 @@ export class API { const fragmentSize = 1e7; // 10 MB for (let i = 0, ii = messageString.length; i < ii; i += fragmentSize) { const data = messageString.substring(i, i + fragmentSize); - port.postMessage({action: 'fragment', data}); + port.postMessage(/** @type {import('backend').InvokeWithProgressRequestFragmentMessage} */ ({action: 'fragment', data})); } - port.postMessage({action: 'invoke'}); + port.postMessage(/** @type {import('backend').InvokeWithProgressRequestInvokeMessage} */ ({action: 'invoke'})); } catch (e) { cleanup(); reject(e); - } finally { - action = null; - params = null; } })(); }); } + /** + * @template [TReturn=unknown] + * @param {string} action + * @param {import('core').SerializableObject} [params] + * @returns {Promise} + */ _invoke(action, params={}) { const data = {action, params}; return new Promise((resolve, reject) => { @@ -303,7 +562,7 @@ export class API { this._checkLastError(chrome.runtime.lastError); if (response !== null && typeof response === 'object') { if (typeof response.error !== 'undefined') { - reject(deserializeError(response.error)); + reject(ExtensionError.deserialize(response.error)); } else { resolve(response.result); } @@ -318,7 +577,10 @@ export class API { }); } - _checkLastError() { + /** + * @param {chrome.runtime.LastError|undefined} _ignore + */ + _checkLastError(_ignore) { // NOP } } diff --git a/ext/js/comm/clipboard-monitor.js b/ext/js/comm/clipboard-monitor.js index c5046046..06e95438 100644 --- a/ext/js/comm/clipboard-monitor.js +++ b/ext/js/comm/clipboard-monitor.js @@ -18,17 +18,32 @@ import {EventDispatcher} from '../core.js'; +/** + * @augments EventDispatcher + */ export class ClipboardMonitor extends EventDispatcher { + /** + * @param {{japaneseUtil: import('../language/sandbox/japanese-util.js').JapaneseUtil, clipboardReader: import('clipboard-monitor').ClipboardReaderLike}} details + */ constructor({japaneseUtil, clipboardReader}) { super(); + /** @type {import('../language/sandbox/japanese-util.js').JapaneseUtil} */ this._japaneseUtil = japaneseUtil; + /** @type {import('clipboard-monitor').ClipboardReaderLike} */ this._clipboardReader = clipboardReader; + /** @type {?number} */ this._timerId = null; + /** @type {?import('core').TokenObject} */ this._timerToken = null; + /** @type {number} */ this._interval = 250; + /** @type {?string} */ this._previousText = null; } + /** + * @returns {void} + */ start() { this.stop(); @@ -36,6 +51,7 @@ export class ClipboardMonitor extends EventDispatcher { // hasn't been started during the await call. The check below the await call // will exit early if the reference has changed. let canChange = false; + /** @type {?import('core').TokenObject} */ const token = {}; const intervalCallback = async () => { this._timerId = null; @@ -55,7 +71,7 @@ export class ClipboardMonitor extends EventDispatcher { ) { this._previousText = text; if (canChange && this._japaneseUtil.isStringPartiallyJapanese(text)) { - this.trigger('change', {text}); + this.trigger('change', /** @type {import('clipboard-monitor').ChangeEvent} */ ({text})); } } @@ -68,6 +84,9 @@ export class ClipboardMonitor extends EventDispatcher { intervalCallback(); } + /** + * @returns {void} + */ stop() { this._timerToken = null; this._previousText = null; @@ -77,6 +96,9 @@ export class ClipboardMonitor extends EventDispatcher { } } + /** + * @param {?string} text + */ setPreviousText(text) { this._previousText = text; } diff --git a/ext/js/comm/clipboard-reader.js b/ext/js/comm/clipboard-reader.js index 8139cc11..c7b45a7c 100644 --- a/ext/js/comm/clipboard-reader.js +++ b/ext/js/comm/clipboard-reader.js @@ -24,17 +24,20 @@ import {MediaUtil} from '../media/media-util.js'; export class ClipboardReader { /** * Creates a new instances of a clipboard reader. - * @param {object} details Details about how to set up the instance. - * @param {?Document} details.document The Document object to be used, or null for no support. - * @param {?string} details.pasteTargetSelector The selector for the paste target element. - * @param {?string} details.richContentPasteTargetSelector The selector for the rich content paste target element. + * @param {{document: ?Document, pasteTargetSelector: ?string, richContentPasteTargetSelector: ?string}} details Details about how to set up the instance. */ constructor({document=null, pasteTargetSelector=null, richContentPasteTargetSelector=null}) { + /** @type {?Document} */ this._document = document; + /** @type {?string} */ this._browser = null; + /** @type {?HTMLTextAreaElement} */ this._pasteTarget = null; + /** @type {?string} */ this._pasteTargetSelector = pasteTargetSelector; + /** @type {?HTMLElement} */ this._richContentPasteTarget = null; + /** @type {?string} */ this._richContentPasteTargetSelector = richContentPasteTargetSelector; } @@ -56,7 +59,7 @@ export class ClipboardReader { /** * Gets the text in the clipboard. * @param {boolean} useRichText Whether or not to use rich text for pasting, when possible. - * @returns {string} A string containing the clipboard text. + * @returns {Promise} A string containing the clipboard text. * @throws {Error} Error if not supported. */ async getText(useRichText) { @@ -90,7 +93,7 @@ export class ClipboardReader { const target = this._getRichContentPasteTarget(); target.focus(); document.execCommand('paste'); - const result = target.textContent; + const result = /** @type {string} */ (target.textContent); this._clearRichContent(target); return result; } else { @@ -106,7 +109,7 @@ export class ClipboardReader { /** * Gets the first image in the clipboard. - * @returns {string} A string containing a data URL of the image file, or null if no image was found. + * @returns {Promise} A string containing a data URL of the image file, or null if no image was found. * @throws {Error} Error if not supported. */ async getImage() { @@ -155,35 +158,62 @@ export class ClipboardReader { // Private + /** + * @returns {boolean} + */ _isFirefox() { return (this._browser === 'firefox' || this._browser === 'firefox-mobile'); } + /** + * @param {Blob} file + * @returns {Promise} + */ _readFileAsDataURL(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); - reader.onload = () => resolve(reader.result); + reader.onload = () => resolve(/** @type {string} */ (reader.result)); reader.onerror = () => reject(reader.error); reader.readAsDataURL(file); }); } + /** + * @returns {HTMLTextAreaElement} + */ _getPasteTarget() { - if (this._pasteTarget === null) { this._pasteTarget = this._findPasteTarget(this._pasteTargetSelector); } + if (this._pasteTarget === null) { + this._pasteTarget = /** @type {HTMLTextAreaElement} */ (this._findPasteTarget(this._pasteTargetSelector)); + } return this._pasteTarget; } + /** + * @returns {HTMLElement} + */ _getRichContentPasteTarget() { - if (this._richContentPasteTarget === null) { this._richContentPasteTarget = this._findPasteTarget(this._richContentPasteTargetSelector); } + if (this._richContentPasteTarget === null) { + this._richContentPasteTarget = /** @type {HTMLElement} */ (this._findPasteTarget(this._richContentPasteTargetSelector)); + } return this._richContentPasteTarget; } + /** + * @template {Element} T + * @param {?string} selector + * @returns {T} + * @throws {Error} + */ _findPasteTarget(selector) { - const target = this._document.querySelector(selector); + if (selector === null) { throw new Error('Invalid selector'); } + const target = this._document !== null ? this._document.querySelector(selector) : null; if (target === null) { throw new Error('Clipboard paste target does not exist'); } - return target; + return /** @type {T} */ (target); } + /** + * @param {HTMLElement} element + */ _clearRichContent(element) { for (const image of element.querySelectorAll('img')) { image.removeAttribute('src'); diff --git a/ext/js/comm/cross-frame-api.js b/ext/js/comm/cross-frame-api.js index fe220f21..34f3f36a 100644 --- a/ext/js/comm/cross-frame-api.js +++ b/ext/js/comm/cross-frame-api.js @@ -16,34 +16,66 @@ * along with this program. If not, see . */ -import {EventDispatcher, EventListenerCollection, deserializeError, invokeMessageHandler, log, serializeError} from '../core.js'; +import {EventDispatcher, EventListenerCollection, invokeMessageHandler, log} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js'; import {yomitan} from '../yomitan.js'; +/** + * @augments EventDispatcher + */ class CrossFrameAPIPort extends EventDispatcher { + /** + * @param {?number} otherTabId + * @param {number} otherFrameId + * @param {chrome.runtime.Port} port + * @param {import('core').MessageHandlerMap} messageHandlers + */ constructor(otherTabId, otherFrameId, port, messageHandlers) { super(); + /** @type {?number} */ this._otherTabId = otherTabId; + /** @type {number} */ this._otherFrameId = otherFrameId; + /** @type {?chrome.runtime.Port} */ this._port = port; + /** @type {import('core').MessageHandlerMap} */ this._messageHandlers = messageHandlers; + /** @type {Map} */ this._activeInvocations = new Map(); + /** @type {number} */ this._invocationId = 0; + /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); } + /** @type {?number} */ get otherTabId() { return this._otherTabId; } + /** @type {number} */ get otherFrameId() { return this._otherFrameId; } + /** + * @throws {Error} + */ prepare() { + if (this._port === null) { throw new Error('Invalid state'); } this._eventListeners.addListener(this._port.onDisconnect, this._onDisconnect.bind(this)); this._eventListeners.addListener(this._port.onMessage, this._onMessage.bind(this)); } + /** + * @template [TParams=import('core').SerializableObject] + * @template [TReturn=unknown] + * @param {string} action + * @param {TParams} params + * @param {number} ackTimeout + * @param {number} responseTimeout + * @returns {Promise} + */ invoke(action, params, ackTimeout, responseTimeout) { return new Promise((resolve, reject) => { if (this._port === null) { @@ -52,6 +84,7 @@ class CrossFrameAPIPort extends EventDispatcher { } const id = this._invocationId++; + /** @type {import('cross-frame-api').Invocation} */ const invocation = { id, resolve, @@ -73,19 +106,21 @@ class CrossFrameAPIPort extends EventDispatcher { } try { - this._port.postMessage({type: 'invoke', id, data: {action, params}}); + this._port.postMessage(/** @type {import('cross-frame-api').InvokeMessage} */ ({type: 'invoke', id, data: {action, params}})); } catch (e) { this._onError(id, e); } }); } + /** */ disconnect() { this._onDisconnect(); } // Private + /** */ _onDisconnect() { if (this._port === null) { return; } this._eventListeners.removeAllEventListeners(); @@ -96,22 +131,29 @@ class CrossFrameAPIPort extends EventDispatcher { this.trigger('disconnect', this); } - _onMessage({type, id, data}) { + /** + * @param {import('cross-frame-api').Message} details + */ + _onMessage(details) { + const {type, id} = details; switch (type) { case 'invoke': - this._onInvoke(id, data); + this._onInvoke(id, details.data); break; case 'ack': this._onAck(id); break; case 'result': - this._onResult(id, data); + this._onResult(id, details.data); break; } } // Response handlers + /** + * @param {number} id + */ _onAck(id) { const invocation = this._activeInvocations.get(id); if (typeof invocation === 'undefined') { @@ -141,6 +183,10 @@ class CrossFrameAPIPort extends EventDispatcher { } } + /** + * @param {number} id + * @param {import('core').Response} data + */ _onResult(id, data) { const invocation = this._activeInvocations.get(id); if (typeof invocation === 'undefined') { @@ -162,17 +208,21 @@ class CrossFrameAPIPort extends EventDispatcher { const error = data.error; if (typeof error !== 'undefined') { - invocation.reject(deserializeError(error)); + invocation.reject(ExtensionError.deserialize(error)); } else { invocation.resolve(data.result); } } + /** + * @param {number} id + * @param {unknown} error + */ _onError(id, error) { const invocation = this._activeInvocations.get(id); if (typeof invocation === 'undefined') { return; } - if (typeof error === 'string') { + if (!(error instanceof Error)) { error = new Error(`${error} (${invocation.action})`); } @@ -186,6 +236,11 @@ class CrossFrameAPIPort extends EventDispatcher { // Invocation + /** + * @param {number} id + * @param {import('cross-frame-api').InvocationData} details + * @returns {boolean} + */ _onInvoke(id, {action, params}) { const messageHandler = this._messageHandlers.get(action); this._sendAck(id); @@ -194,10 +249,17 @@ class CrossFrameAPIPort extends EventDispatcher { return false; } + /** + * @param {import('core').Response} data + * @returns {void} + */ const callback = (data) => this._sendResult(id, data); return invokeMessageHandler(messageHandler, params, callback); } + /** + * @param {import('cross-frame-api').Message} data + */ _sendResponse(data) { if (this._port === null) { return; } try { @@ -207,45 +269,85 @@ class CrossFrameAPIPort extends EventDispatcher { } } + /** + * @param {number} id + */ _sendAck(id) { this._sendResponse({type: 'ack', id}); } + /** + * @param {number} id + * @param {import('core').Response} data + */ _sendResult(id, data) { this._sendResponse({type: 'result', id, data}); } + /** + * @param {number} id + * @param {Error} error + */ _sendError(id, error) { - this._sendResponse({type: 'result', id, data: {error: serializeError(error)}}); + this._sendResponse({type: 'result', id, data: {error: ExtensionError.serialize(error)}}); } } export class CrossFrameAPI { constructor() { + /** @type {number} */ this._ackTimeout = 3000; // 3 seconds + /** @type {number} */ this._responseTimeout = 10000; // 10 seconds + /** @type {Map>} */ this._commPorts = new Map(); + /** @type {import('core').MessageHandlerMap} */ this._messageHandlers = new Map(); + /** @type {(port: CrossFrameAPIPort) => void} */ this._onDisconnectBind = this._onDisconnect.bind(this); + /** @type {?number} */ this._tabId = null; + /** @type {?number} */ this._frameId = null; } + /** */ async prepare() { chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); - ({tabId: this._tabId, frameId: this._frameId} = await yomitan.api.frameInformationGet()); + ({tabId: this._tabId = null, frameId: this._frameId = null} = await yomitan.api.frameInformationGet()); } - invoke(targetFrameId, action, params={}) { + /** + * @template [TParams=import('core').SerializableObject] + * @template [TReturn=unknown] + * @param {number} targetFrameId + * @param {string} action + * @param {TParams} params + * @returns {Promise} + */ + invoke(targetFrameId, action, params) { return this.invokeTab(null, targetFrameId, action, params); } - async invokeTab(targetTabId, targetFrameId, action, params={}) { + /** + * @template [TParams=import('core').SerializableObject] + * @template [TReturn=unknown] + * @param {?number} targetTabId + * @param {number} targetFrameId + * @param {string} action + * @param {TParams} params + * @returns {Promise} + */ + async invokeTab(targetTabId, targetFrameId, action, params) { if (typeof targetTabId !== 'number') { targetTabId = this._tabId; } const commPort = await this._getOrCreateCommPort(targetTabId, targetFrameId); return await commPort.invoke(action, params, this._ackTimeout, this._responseTimeout); } + /** + * @param {import('core').MessageHandlerArray} messageHandlers + * @throws {Error} + */ registerHandlers(messageHandlers) { for (const [key, value] of messageHandlers) { if (this._messageHandlers.has(key)) { @@ -255,12 +357,19 @@ export class CrossFrameAPI { } } + /** + * @param {string} key + * @returns {boolean} + */ unregisterHandler(key) { return this._messageHandlers.delete(key); } // Private + /** + * @param {chrome.runtime.Port} port + */ _onConnect(port) { try { let details; @@ -280,6 +389,9 @@ export class CrossFrameAPI { } } + /** + * @param {CrossFrameAPIPort} commPort + */ _onDisconnect(commPort) { commPort.off('disconnect', this._onDisconnectBind); const {otherTabId, otherFrameId} = commPort; @@ -292,7 +404,12 @@ export class CrossFrameAPI { } } - _getOrCreateCommPort(otherTabId, otherFrameId) { + /** + * @param {?number} otherTabId + * @param {number} otherFrameId + * @returns {Promise} + */ + async _getOrCreateCommPort(otherTabId, otherFrameId) { const tabPorts = this._commPorts.get(otherTabId); if (typeof tabPorts !== 'undefined') { const commPort = tabPorts.get(otherFrameId); @@ -300,9 +417,13 @@ export class CrossFrameAPI { return commPort; } } - return this._createCommPort(otherTabId, otherFrameId); + return await this._createCommPort(otherTabId, otherFrameId); } - + /** + * @param {?number} otherTabId + * @param {number} otherFrameId + * @returns {Promise} + */ async _createCommPort(otherTabId, otherFrameId) { await yomitan.api.openCrossFramePort(otherTabId, otherFrameId); @@ -313,8 +434,15 @@ export class CrossFrameAPI { return commPort; } } + throw new Error('Comm port didn\'t open'); } + /** + * @param {?number} otherTabId + * @param {number} otherFrameId + * @param {chrome.runtime.Port} port + * @returns {CrossFrameAPIPort} + */ _setupCommPort(otherTabId, otherFrameId, port) { const commPort = new CrossFrameAPIPort(otherTabId, otherFrameId, port, this._messageHandlers); let tabPorts = this._commPorts.get(otherTabId); diff --git a/ext/js/comm/frame-ancestry-handler.js b/ext/js/comm/frame-ancestry-handler.js index eeefac3f..49c96c22 100644 --- a/ext/js/comm/frame-ancestry-handler.js +++ b/ext/js/comm/frame-ancestry-handler.js @@ -31,11 +31,17 @@ export class FrameAncestryHandler { * @param {number} frameId The frame ID of the current frame the instance is instantiated in. */ constructor(frameId) { + /** @type {number} */ this._frameId = frameId; + /** @type {boolean} */ this._isPrepared = false; + /** @type {string} */ this._requestMessageId = 'FrameAncestryHandler.requestFrameInfo'; + /** @type {string} */ this._responseMessageIdBase = `${this._requestMessageId}.response.`; + /** @type {?Promise} */ this._getFrameAncestryInfoPromise = null; + /** @type {Map} */ this._childFrameMap = new Map(); } @@ -68,7 +74,7 @@ export class FrameAncestryHandler { * Gets the frame ancestry information for the current frame. If the frame is the * root frame, an empty array is returned. Otherwise, an array of frame IDs is returned, * starting from the nearest ancestor. - * @returns {number[]} An array of frame IDs corresponding to the ancestors of the current frame. + * @returns {Promise} An array of frame IDs corresponding to the ancestors of the current frame. */ async getFrameAncestryInfo() { if (this._getFrameAncestryInfoPromise === null) { @@ -82,7 +88,7 @@ export class FrameAncestryHandler { * For this function to work, the `getFrameAncestryInfo` function needs to have * been invoked previously. * @param {number} frameId The frame ID of the child frame to get. - * @returns {HTMLElement} The element corresponding to the frame with ID `frameId`, otherwise `null`. + * @returns {?Element} The element corresponding to the frame with ID `frameId`, otherwise `null`. */ getChildFrameElement(frameId) { const frameInfo = this._childFrameMap.get(frameId); @@ -99,6 +105,10 @@ export class FrameAncestryHandler { // Private + /** + * @param {number} [timeout] + * @returns {Promise} + */ _getFrameAncestryInfo(timeout=5000) { return new Promise((resolve, reject) => { const targetWindow = window.parent; @@ -110,7 +120,9 @@ export class FrameAncestryHandler { const uniqueId = generateId(16); let nonce = generateId(16); const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`; + /** @type {number[]} */ const results = []; + /** @type {?number} */ let timer = null; const cleanup = () => { @@ -120,6 +132,10 @@ export class FrameAncestryHandler { } yomitan.crossFrame.unregisterHandler(responseMessageId); }; + /** + * @param {import('frame-ancestry-handler').RequestFrameInfoResponseParams} params + * @returns {?import('frame-ancestry-handler').RequestFrameInfoResponseReturn} + */ const onMessage = (params) => { if (params.nonce !== nonce) { return null; } @@ -155,24 +171,35 @@ export class FrameAncestryHandler { }); } + /** + * @param {MessageEvent} event + */ _onWindowMessage(event) { - const {source} = event; - if (source === window || source.parent !== window) { return; } + const source = /** @type {?Window} */ (event.source); + if (source === null || source === window || source.parent !== window) { return; } const {data} = event; - if ( - typeof data === 'object' && - data !== null && - data.action === this._requestMessageId - ) { - this._onRequestFrameInfo(data.params, source); - } + if (typeof data !== 'object' || data === null) { return; } + + const {action} = /** @type {import('core').SerializableObject} */ (data); + if (action !== this._requestMessageId) { return; } + + const {params} = /** @type {import('core').SerializableObject} */ (data); + if (typeof params !== 'object' || params === null) { return; } + + this._onRequestFrameInfo(/** @type {import('core').SerializableObject} */ (params), source); } + /** + * @param {import('core').SerializableObject} params + * @param {Window} source + */ async _onRequestFrameInfo(params, source) { try { let {originFrameId, childFrameId, uniqueId, nonce} = params; if ( + typeof originFrameId !== 'number' || + typeof childFrameId !== 'number' || !this._isNonNegativeInteger(originFrameId) || typeof uniqueId !== 'string' || typeof nonce !== 'string' @@ -183,13 +210,17 @@ export class FrameAncestryHandler { const frameId = this._frameId; const {parent} = window; const more = (window !== parent); + /** @type {import('frame-ancestry-handler').RequestFrameInfoResponseParams} */ const responseParams = {frameId, nonce, more}; const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`; try { + /** @type {?import('frame-ancestry-handler').RequestFrameInfoResponseReturn} */ const response = await yomitan.crossFrame.invoke(originFrameId, responseMessageId, responseParams); if (response === null) { return; } - nonce = response.nonce; + const nonce2 = response.nonce; + if (typeof nonce2 !== 'string') { return; } + nonce = nonce2; } catch (e) { return; } @@ -199,13 +230,20 @@ export class FrameAncestryHandler { } if (more) { - this._requestFrameInfo(parent, originFrameId, frameId, uniqueId, nonce); + this._requestFrameInfo(parent, originFrameId, frameId, uniqueId, /** @type {string} */ (nonce)); } } catch (e) { // NOP } } + /** + * @param {Window} targetWindow + * @param {number} originFrameId + * @param {number} childFrameId + * @param {string} uniqueId + * @param {string} nonce + */ _requestFrameInfo(targetWindow, originFrameId, childFrameId, uniqueId, nonce) { targetWindow.postMessage({ action: this._requestMessageId, @@ -213,15 +251,22 @@ export class FrameAncestryHandler { }, '*'); } + /** + * @param {number} value + * @returns {boolean} + */ _isNonNegativeInteger(value) { return ( - typeof value === 'number' && Number.isFinite(value) && value >= 0 && Math.floor(value) === value ); } + /** + * @param {Window} contentWindow + * @returns {?Element} + */ _findFrameElementWithContentWindow(contentWindow) { // Check frameElement, for non-null same-origin frames try { @@ -232,9 +277,9 @@ export class FrameAncestryHandler { } // Check frames - const frameTypes = ['iframe', 'frame', 'embed']; + const frameTypes = ['iframe', 'frame', 'object']; for (const frameType of frameTypes) { - for (const frame of document.getElementsByTagName(frameType)) { + for (const frame of /** @type {HTMLCollectionOf} */ (document.getElementsByTagName(frameType))) { if (frame.contentWindow === contentWindow) { return frame; } @@ -242,20 +287,24 @@ export class FrameAncestryHandler { } // Check for shadow roots + /** @type {Node[]} */ const rootElements = [document.documentElement]; while (rootElements.length > 0) { - const rootElement = rootElements.shift(); + const rootElement = /** @type {Node} */ (rootElements.shift()); const walker = document.createTreeWalker(rootElement, NodeFilter.SHOW_ELEMENT); while (walker.nextNode()) { - const element = walker.currentNode; + const element = /** @type {Element} */ (walker.currentNode); + // @ts-ignore - this is more simple to elide any type checks or casting if (element.contentWindow === contentWindow) { return element; } + /** @type {?ShadowRoot|undefined} */ const shadowRoot = ( element.shadowRoot || - element.openOrClosedShadowRoot // Available to Firefox 63+ for WebExtensions + // @ts-ignore - openOrClosedShadowRoot is available to Firefox 63+ for WebExtensions + element.openOrClosedShadowRoot ); if (shadowRoot) { rootElements.push(shadowRoot); diff --git a/ext/js/comm/frame-client.js b/ext/js/comm/frame-client.js index 0ca37feb..1519cf7f 100644 --- a/ext/js/comm/frame-client.js +++ b/ext/js/comm/frame-client.js @@ -20,47 +20,81 @@ import {deferPromise, generateId, isObject} from '../core.js'; export class FrameClient { constructor() { + /** @type {?string} */ this._secret = null; + /** @type {?string} */ this._token = null; + /** @type {?number} */ this._frameId = null; } + /** @type {number} */ get frameId() { + if (this._frameId === null) { throw new Error('Not connected'); } return this._frameId; } + /** + * @param {import('extension').HtmlElementWithContentWindow} frame + * @param {string} targetOrigin + * @param {number} hostFrameId + * @param {import('frame-client').SetupFrameFunction} setupFrame + * @param {number} [timeout] + */ async connect(frame, targetOrigin, hostFrameId, setupFrame, timeout=10000) { - const {secret, token, frameId} = await this._connectIternal(frame, targetOrigin, hostFrameId, setupFrame, timeout); + const {secret, token, frameId} = await this._connectInternal(frame, targetOrigin, hostFrameId, setupFrame, timeout); this._secret = secret; this._token = token; this._frameId = frameId; } + /** + * @returns {boolean} + */ isConnected() { return (this._secret !== null); } + /** + * @template T + * @param {T} data + * @returns {import('frame-client').Message} + * @throws {Error} + */ createMessage(data) { if (!this.isConnected()) { throw new Error('Not connected'); } return { - token: this._token, - secret: this._secret, + token: /** @type {string} */ (this._token), + secret: /** @type {string} */ (this._secret), data }; } - _connectIternal(frame, targetOrigin, hostFrameId, setupFrame, timeout) { + /** + * @param {import('extension').HtmlElementWithContentWindow} frame + * @param {string} targetOrigin + * @param {number} hostFrameId + * @param {(frame: import('extension').HtmlElementWithContentWindow) => void} setupFrame + * @param {number} timeout + * @returns {Promise<{secret: string, token: string, frameId: number}>} + */ + _connectInternal(frame, targetOrigin, hostFrameId, setupFrame, timeout) { return new Promise((resolve, reject) => { const tokenMap = new Map(); + /** @type {?number} */ let timer = null; - let { - promise: frameLoadedPromise, - resolve: frameLoadedResolve, - reject: frameLoadedReject - } = deferPromise(); - + const deferPromiseDetails = /** @type {import('core').DeferredPromiseDetails} */ (deferPromise()); + const frameLoadedPromise = deferPromiseDetails.promise; + let frameLoadedResolve = /** @type {?() => void} */ (deferPromiseDetails.resolve); + let frameLoadedReject = /** @type {?(reason?: import('core').RejectionReason) => void} */ (deferPromiseDetails.reject); + + /** + * @param {string} action + * @param {import('core').SerializableObject} params + * @throws {Error} + */ const postMessage = (action, params) => { const contentWindow = frame.contentWindow; if (contentWindow === null) { throw new Error('Frame missing content window'); } @@ -76,11 +110,15 @@ export class FrameClient { contentWindow.postMessage({action, params}, targetOrigin); }; + /** @type {import('extension').ChromeRuntimeOnMessageCallback} */ const onMessage = (message) => { onMessageInner(message); return false; }; + /** + * @param {import('extension').ChromeRuntimeMessageWithFrameId} message + */ const onMessageInner = async (message) => { try { if (!isObject(message)) { return; } @@ -92,7 +130,7 @@ export class FrameClient { switch (action) { case 'frameEndpointReady': { - const {secret} = params; + const {secret} = /** @type {import('frame-client').FrameEndpointReadyDetails} */ (params); const token = generateId(16); tokenMap.set(secret, token); postMessage('frameEndpointConnect', {secret, token, hostFrameId}); @@ -100,10 +138,10 @@ export class FrameClient { break; case 'frameEndpointConnected': { - const {secret, token} = params; + const {secret, token} = /** @type {import('frame-client').FrameEndpointConnectedDetails} */ (params); const frameId = message.frameId; const token2 = tokenMap.get(secret); - if (typeof token2 !== 'undefined' && token === token2) { + if (typeof token2 !== 'undefined' && token === token2 && typeof frameId === 'number') { cleanup(); resolve({secret, token, frameId}); } @@ -168,6 +206,10 @@ export class FrameClient { }); } + /** + * @param {import('extension').HtmlElementWithContentWindow} frame + * @returns {boolean} + */ static isFrameAboutBlank(frame) { try { const contentDocument = frame.contentDocument; diff --git a/ext/js/comm/frame-endpoint.js b/ext/js/comm/frame-endpoint.js index 5555e60f..c338e143 100644 --- a/ext/js/comm/frame-endpoint.js +++ b/ext/js/comm/frame-endpoint.js @@ -16,50 +16,73 @@ * along with this program. If not, see . */ -import {EventListenerCollection, generateId, isObject} from '../core.js'; +import {EventListenerCollection, generateId} from '../core.js'; import {yomitan} from '../yomitan.js'; export class FrameEndpoint { constructor() { + /** @type {string} */ this._secret = generateId(16); + /** @type {?string} */ this._token = null; + /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); + /** @type {boolean} */ this._eventListenersSetup = false; } + /** + * @returns {void} + */ signal() { if (!this._eventListenersSetup) { this._eventListeners.addEventListener(window, 'message', this._onMessage.bind(this), false); this._eventListenersSetup = true; } - yomitan.api.broadcastTab('frameEndpointReady', {secret: this._secret}); + /** @type {import('frame-client').FrameEndpointReadyDetails} */ + const details = {secret: this._secret}; + yomitan.api.broadcastTab('frameEndpointReady', details); } + /** + * @param {unknown} message + * @returns {boolean} + */ authenticate(message) { return ( this._token !== null && - isObject(message) && - this._token === message.token && - this._secret === message.secret + typeof message === 'object' && message !== null && + this._token === /** @type {import('core').SerializableObject} */ (message).token && + this._secret === /** @type {import('core').SerializableObject} */ (message).secret ); } - _onMessage(e) { + /** + * @param {MessageEvent} event + */ + _onMessage(event) { if (this._token !== null) { return; } // Already initialized - const data = e.data; - if (!isObject(data) || data.action !== 'frameEndpointConnect') { return; } // Invalid message + const {data} = event; + if (typeof data !== 'object' || data === null) { return; } // Invalid message - const params = data.params; - if (!isObject(params)) { return; } // Invalid data + const {action} = /** @type {import('core').SerializableObject} */ (data); + if (action !== 'frameEndpointConnect') { return; } // Invalid message - const secret = params.secret; + const {params} = /** @type {import('core').SerializableObject} */ (data); + if (typeof params !== 'object' || params === null) { return; } // Invalid data + + const {secret} = /** @type {import('core').SerializableObject} */ (params); if (secret !== this._secret) { return; } // Invalid authentication - const {token, hostFrameId} = params; + const {token, hostFrameId} = /** @type {import('core').SerializableObject} */ (params); + if (typeof token !== 'string' || typeof hostFrameId !== 'number') { return; } // Invalid target + this._token = token; this._eventListeners.removeAllEventListeners(); - yomitan.api.sendMessageToFrame(hostFrameId, 'frameEndpointConnected', {secret, token}); + /** @type {import('frame-client').FrameEndpointConnectedDetails} */ + const details = {secret, token}; + yomitan.api.sendMessageToFrame(hostFrameId, 'frameEndpointConnected', details); } } diff --git a/ext/js/comm/frame-offset-forwarder.js b/ext/js/comm/frame-offset-forwarder.js index ef75f1d0..af9bd268 100644 --- a/ext/js/comm/frame-offset-forwarder.js +++ b/ext/js/comm/frame-offset-forwarder.js @@ -20,11 +20,19 @@ import {yomitan} from '../yomitan.js'; import {FrameAncestryHandler} from './frame-ancestry-handler.js'; export class FrameOffsetForwarder { + /** + * @param {number} frameId + */ constructor(frameId) { + /** @type {number} */ this._frameId = frameId; + /** @type {FrameAncestryHandler} */ this._frameAncestryHandler = new FrameAncestryHandler(frameId); } + /** + * @returns {void} + */ prepare() { this._frameAncestryHandler.prepare(); yomitan.crossFrame.registerHandlers([ @@ -32,6 +40,9 @@ export class FrameOffsetForwarder { ]); } + /** + * @returns {Promise} + */ async getOffset() { if (this._frameAncestryHandler.isRootFrame()) { return [0, 0]; @@ -41,6 +52,7 @@ export class FrameOffsetForwarder { const ancestorFrameIds = await this._frameAncestryHandler.getFrameAncestryInfo(); let childFrameId = this._frameId; + /** @type {Promise[]} */ const promises = []; for (const frameId of ancestorFrameIds) { promises.push(yomitan.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId})); @@ -64,6 +76,10 @@ export class FrameOffsetForwarder { // Private + /** + * @param {{frameId: number}} event + * @returns {?import('frame-offset-forwarder').ChildFrameRect} + */ _onMessageGetChildFrameRect({frameId}) { const frameElement = this._frameAncestryHandler.getChildFrameElement(frameId); if (frameElement === null) { return null; } diff --git a/ext/js/comm/mecab.js b/ext/js/comm/mecab.js index c7314605..072b42f3 100644 --- a/ext/js/comm/mecab.js +++ b/ext/js/comm/mecab.js @@ -23,33 +23,27 @@ import {EventListenerCollection} from '../core.js'; * used to parse text into individual terms. */ export class Mecab { - /** - * The resulting data from an invocation of `parseText`. - * @typedef {object} ParseResult - * @property {string} name The dictionary name for the parsed result. - * @property {ParseTerm[]} lines The resulting parsed terms. - */ - - /** - * A fragment of the parsed text. - * @typedef {object} ParseFragment - * @property {string} term The term. - * @property {string} reading The reading of the term. - * @property {string} source The source text. - */ - /** * Creates a new instance of the class. */ constructor() { + /** @type {?chrome.runtime.Port} */ this._port = null; + /** @type {number} */ this._sequence = 0; + /** @type {Map void, reject: (reason?: unknown) => void, timer: number}>} */ this._invocations = new Map(); + /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); + /** @type {number} */ this._timeout = 5000; + /** @type {number} */ this._version = 1; + /** @type {?number} */ this._remoteVersion = null; + /** @type {boolean} */ this._enabled = false; + /** @type {?Promise} */ this._setupPortPromise = null; } @@ -107,7 +101,7 @@ export class Mecab { /** * Gets the version of the MeCab component. - * @returns {?number} The version of the MeCab component, or `null` if the component was not found. + * @returns {Promise} The version of the MeCab component, or `null` if the component was not found. */ async getVersion() { try { @@ -135,17 +129,26 @@ export class Mecab { * ] * ``` * @param {string} text The string to parse. - * @returns {ParseResult[]} A collection of parsing results of the text. + * @returns {Promise} A collection of parsing results of the text. */ async parseText(text) { await this._setupPort(); const rawResults = await this._invoke('parse_text', {text}); - return this._convertParseTextResults(rawResults); + // Note: The format of rawResults is not validated + return this._convertParseTextResults(/** @type {import('mecab').ParseResultRaw} */ (rawResults)); } // Private - _onMessage({sequence, data}) { + /** + * @param {unknown} message + */ + _onMessage(message) { + if (typeof message !== 'object' || message === null) { return; } + + const {sequence, data} = /** @type {import('core').SerializableObject} */ (message); + if (typeof sequence !== 'number') { return; } + const invocation = this._invocations.get(sequence); if (typeof invocation === 'undefined') { return; } @@ -155,6 +158,9 @@ export class Mecab { this._invocations.delete(sequence); } + /** + * @returns {void} + */ _onDisconnect() { if (this._port === null) { return; } const e = chrome.runtime.lastError; @@ -166,10 +172,16 @@ export class Mecab { this._clearPort(); } + /** + * @param {string} action + * @param {import('core').SerializableObject} params + * @returns {Promise} + */ _invoke(action, params) { return new Promise((resolve, reject) => { if (this._port === null) { reject(new Error('Port disconnected')); + return; } const sequence = this._sequence++; @@ -179,15 +191,21 @@ export class Mecab { reject(new Error(`MeCab invoke timed out after ${this._timeout}ms`)); }, this._timeout); - this._invocations.set(sequence, {resolve, reject, timer}, this._timeout); + this._invocations.set(sequence, {resolve, reject, timer}); this._port.postMessage({action, params, sequence}); }); } + /** + * @param {import('mecab').ParseResultRaw} rawResults + * @returns {import('mecab').ParseResult[]} + */ _convertParseTextResults(rawResults) { + /** @type {import('mecab').ParseResult[]} */ const results = []; for (const [name, rawLines] of Object.entries(rawResults)) { + /** @type {import('mecab').ParseFragment[][]} */ const lines = []; for (const rawLine of rawLines) { const line = []; @@ -204,6 +222,9 @@ export class Mecab { return results; } + /** + * @returns {Promise} + */ async _setupPort() { if (!this._enabled) { throw new Error('MeCab not enabled'); @@ -214,10 +235,13 @@ export class Mecab { try { await this._setupPortPromise; } catch (e) { - throw new Error(e.message); + throw new Error(e instanceof Error ? e.message : `${e}`); } } + /** + * @returns {Promise} + */ async _setupPort2() { const port = chrome.runtime.connectNative('yomitan_mecab'); this._eventListeners.addListener(port.onMessage, this._onMessage.bind(this)); @@ -225,7 +249,14 @@ export class Mecab { this._port = port; try { - const {version} = await this._invoke('get_version', {}); + const data = await this._invoke('get_version', {}); + if (typeof data !== 'object' || data === null) { + throw new Error('Invalid version'); + } + const {version} = /** @type {import('core').SerializableObject} */ (data); + if (typeof version !== 'number') { + throw new Error('Invalid version'); + } this._remoteVersion = version; if (version !== this._version) { throw new Error(`Unsupported MeCab native messenger version ${version}. Yomitan supports version ${this._version}.`); @@ -238,9 +269,14 @@ export class Mecab { } } + /** + * @returns {void} + */ _clearPort() { - this._port.disconnect(); - this._port = null; + if (this._port !== null) { + this._port.disconnect(); + this._port = null; + } this._invocations.clear(); this._eventListeners.removeAllEventListeners(); this._sequence = 0; -- cgit v1.2.3 From aabd761ee9064f6a46703f234e016f31f6441fa0 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Nov 2023 13:36:04 -0500 Subject: Remove unneeded references --- ext/js/accessibility/accessibility-controller.js | 5 ++--- ext/js/accessibility/google-docs-util.js | 1 - ext/js/app/frontend.js | 6 ++---- ext/js/app/popup-proxy.js | 14 ++++++-------- ext/js/app/popup-window.js | 9 ++++----- ext/js/background/backend.js | 1 - ext/js/comm/anki-connect.js | 1 - ext/js/dom/document-util.js | 1 - ext/js/dom/text-source-element.js | 1 - ext/js/dom/text-source-range.js | 1 - ext/js/language/dictionary-worker-handler.js | 1 - ext/js/language/dictionary-worker-media-loader.js | 2 +- ext/js/language/translator.js | 1 - ext/js/templates/template-renderer-proxy.js | 2 +- 14 files changed, 16 insertions(+), 30 deletions(-) (limited to 'ext/js/comm') diff --git a/ext/js/accessibility/accessibility-controller.js b/ext/js/accessibility/accessibility-controller.js index a4239947..8250b369 100644 --- a/ext/js/accessibility/accessibility-controller.js +++ b/ext/js/accessibility/accessibility-controller.js @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import {ScriptManager} from '../background/script-manager.js'; import {log} from '../core.js'; /** @@ -25,10 +24,10 @@ import {log} from '../core.js'; export class AccessibilityController { /** * Creates a new instance. - * @param {ScriptManager} scriptManager An instance of the `ScriptManager` class. + * @param {import('../background/script-manager.js').ScriptManager} scriptManager An instance of the `ScriptManager` class. */ constructor(scriptManager) { - /** @type {ScriptManager} */ + /** @type {import('../background/script-manager.js').ScriptManager} */ this._scriptManager = scriptManager; /** @type {?import('core').TokenObject} */ this._updateGoogleDocsAccessibilityToken = null; diff --git a/ext/js/accessibility/google-docs-util.js b/ext/js/accessibility/google-docs-util.js index 9db45cc1..4321f082 100644 --- a/ext/js/accessibility/google-docs-util.js +++ b/ext/js/accessibility/google-docs-util.js @@ -17,7 +17,6 @@ */ import {DocumentUtil} from '../dom/document-util.js'; -import {TextSourceElement} from '../dom/text-source-element.js'; import {TextSourceRange} from '../dom/text-source-range.js'; /** diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index fec933f8..628c504e 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -21,10 +21,8 @@ import {EventListenerCollection, invokeMessageHandler, log, promiseAnimationFram import {DocumentUtil} from '../dom/document-util.js'; import {TextSourceElement} from '../dom/text-source-element.js'; import {TextSourceRange} from '../dom/text-source-range.js'; -import {HotkeyHandler} from '../input/hotkey-handler.js'; import {TextScanner} from '../language/text-scanner.js'; import {yomitan} from '../yomitan.js'; -import {PopupFactory} from './popup-factory.js'; /** * This is the main class responsible for scanning and handling webpage content. @@ -50,7 +48,7 @@ export class Frontend { }) { /** @type {import('frontend').PageType} */ this._pageType = pageType; - /** @type {PopupFactory} */ + /** @type {import('./popup-factory.js').PopupFactory} */ this._popupFactory = popupFactory; /** @type {number} */ this._depth = depth; @@ -70,7 +68,7 @@ export class Frontend { this._allowRootFramePopupProxy = allowRootFramePopupProxy; /** @type {boolean} */ this._childrenSupported = childrenSupported; - /** @type {HotkeyHandler} */ + /** @type {import('../input/hotkey-handler.js').HotkeyHandler} */ this._hotkeyHandler = hotkeyHandler; /** @type {?import('popup').PopupAny} */ this._popup = null; diff --git a/ext/js/app/popup-proxy.js b/ext/js/app/popup-proxy.js index 9b5b0214..924175e2 100644 --- a/ext/js/app/popup-proxy.js +++ b/ext/js/app/popup-proxy.js @@ -16,10 +16,8 @@ * along with this program. If not, see . */ -import {FrameOffsetForwarder} from '../comm/frame-offset-forwarder.js'; import {EventDispatcher, log} from '../core.js'; import {yomitan} from '../yomitan.js'; -import {Popup} from './popup.js'; /** * This class is a proxy for a Popup that is hosted in a different frame. @@ -44,7 +42,7 @@ export class PopupProxy extends EventDispatcher { this._depth = depth; /** @type {number} */ this._frameId = frameId; - /** @type {?FrameOffsetForwarder} */ + /** @type {?import('../comm/frame-offset-forwarder.js').FrameOffsetForwarder} */ this._frameOffsetForwarder = frameOffsetForwarder; /** @type {number} */ @@ -70,7 +68,7 @@ export class PopupProxy extends EventDispatcher { /** * The parent of the popup, which is always `null` for `PopupProxy` instances, * since any potential parent popups are in a different frame. - * @type {?Popup} + * @type {?import('./popup.js').Popup} */ get parent() { return null; @@ -78,7 +76,7 @@ export class PopupProxy extends EventDispatcher { /** * Attempts to set the parent popup. - * @param {Popup} _value The parent to assign. + * @param {import('./popup.js').Popup} _value The parent to assign. * @throws {Error} Throws an error, since this class doesn't support a direct parent. */ set parent(_value) { @@ -88,7 +86,7 @@ export class PopupProxy extends EventDispatcher { /** * The popup child popup, which is always null for `PopupProxy` instances, * since any potential child popups are in a different frame. - * @type {?Popup} + * @type {?import('./popup.js').Popup} */ get child() { return null; @@ -96,7 +94,7 @@ export class PopupProxy extends EventDispatcher { /** * Attempts to set the child popup. - * @param {Popup} _child The child to assign. + * @param {import('./popup.js').Popup} _child The child to assign. * @throws {Error} Throws an error, since this class doesn't support children. */ set child(_child) { @@ -354,7 +352,7 @@ export class PopupProxy extends EventDispatcher { * @param {number} now */ async _updateFrameOffsetInner(now) { - this._frameOffsetPromise = /** @type {FrameOffsetForwarder} */ (this._frameOffsetForwarder).getOffset(); + this._frameOffsetPromise = /** @type {import('../comm/frame-offset-forwarder.js').FrameOffsetForwarder} */ (this._frameOffsetForwarder).getOffset(); try { const offset = await this._frameOffsetPromise; if (offset !== null) { diff --git a/ext/js/app/popup-window.js b/ext/js/app/popup-window.js index af1ac1e4..9a0f8011 100644 --- a/ext/js/app/popup-window.js +++ b/ext/js/app/popup-window.js @@ -18,7 +18,6 @@ import {EventDispatcher} from '../core.js'; import {yomitan} from '../yomitan.js'; -import {Popup} from './popup.js'; /** * This class represents a popup that is hosted in a new native window. @@ -54,7 +53,7 @@ export class PopupWindow extends EventDispatcher { } /** - * @type {?Popup} + * @type {?import('./popup.js').Popup} */ get parent() { return null; @@ -63,7 +62,7 @@ export class PopupWindow extends EventDispatcher { /** * The parent of the popup, which is always `null` for `PopupWindow` instances, * since any potential parent popups are in a different frame. - * @param {Popup} _value The parent to assign. + * @param {import('./popup.js').Popup} _value The parent to assign. * @throws {Error} Throws an error, since this class doesn't support children. */ set parent(_value) { @@ -73,7 +72,7 @@ export class PopupWindow extends EventDispatcher { /** * The popup child popup, which is always null for `PopupWindow` instances, * since any potential child popups are in a different frame. - * @type {?Popup} + * @type {?import('./popup.js').Popup} */ get child() { return null; @@ -81,7 +80,7 @@ export class PopupWindow extends EventDispatcher { /** * Attempts to set the child popup. - * @param {Popup} _value The child to assign. + * @param {import('./popup.js').Popup} _value The child to assign. * @throws Throws an error, since this class doesn't support children. */ set child(_value) { diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index a8683b3f..14877cf1 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -25,7 +25,6 @@ import {Mecab} from '../comm/mecab.js'; import {clone, deferPromise, generateId, invokeMessageHandler, isObject, log, promiseTimeout} from '../core.js'; import {ExtensionError} from '../core/extension-error.js'; import {AnkiUtil} from '../data/anki-util.js'; -import {JsonSchema} from '../data/json-schema.js'; import {OptionsUtil} from '../data/options-util.js'; import {PermissionsUtil} from '../data/permissions-util.js'; import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index 3262af41..b876703f 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import {isObject} from '../core.js'; import {AnkiUtil} from '../data/anki-util.js'; /** diff --git a/ext/js/dom/document-util.js b/ext/js/dom/document-util.js index f53d55fd..cf58d39f 100644 --- a/ext/js/dom/document-util.js +++ b/ext/js/dom/document-util.js @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import {EventListenerCollection} from '../core.js'; import {DOMTextScanner} from './dom-text-scanner.js'; import {TextSourceElement} from './text-source-element.js'; import {TextSourceRange} from './text-source-range.js'; diff --git a/ext/js/dom/text-source-element.js b/ext/js/dom/text-source-element.js index fbe89a61..47c18e30 100644 --- a/ext/js/dom/text-source-element.js +++ b/ext/js/dom/text-source-element.js @@ -18,7 +18,6 @@ import {StringUtil} from '../data/sandbox/string-util.js'; import {DocumentUtil} from './document-util.js'; -import {TextSourceRange} from './text-source-range.js'; /** * This class represents a text source that is attached to a HTML element, such as an diff --git a/ext/js/dom/text-source-range.js b/ext/js/dom/text-source-range.js index 5c3d4184..5dbbd636 100644 --- a/ext/js/dom/text-source-range.js +++ b/ext/js/dom/text-source-range.js @@ -18,7 +18,6 @@ import {DocumentUtil} from './document-util.js'; import {DOMTextScanner} from './dom-text-scanner.js'; -import {TextSourceElement} from './text-source-element.js'; /** * This class represents a text source that comes from text nodes in the document. diff --git a/ext/js/language/dictionary-worker-handler.js b/ext/js/language/dictionary-worker-handler.js index 8ac342b2..3a85cb71 100644 --- a/ext/js/language/dictionary-worker-handler.js +++ b/ext/js/language/dictionary-worker-handler.js @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -import {serializeError} from '../core.js'; import {DictionaryDatabase} from './dictionary-database.js'; import {DictionaryImporter} from './dictionary-importer.js'; import {DictionaryWorkerMediaLoader} from './dictionary-worker-media-loader.js'; diff --git a/ext/js/language/dictionary-worker-media-loader.js b/ext/js/language/dictionary-worker-media-loader.js index 9e3fd67e..2701389e 100644 --- a/ext/js/language/dictionary-worker-media-loader.js +++ b/ext/js/language/dictionary-worker-media-loader.js @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import {deserializeError, generateId} from '../core.js'; +import {generateId} from '../core.js'; /** * Class used for loading and validating media from a worker thread diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 9b01c1ff..67cc53c6 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -19,7 +19,6 @@ import {RegexUtil} from '../general/regex-util.js'; import {TextSourceMap} from '../general/text-source-map.js'; import {Deinflector} from './deinflector.js'; -import {DictionaryDatabase} from './dictionary-database.js'; /** * Class which finds term and kanji dictionary entries for text. diff --git a/ext/js/templates/template-renderer-proxy.js b/ext/js/templates/template-renderer-proxy.js index 6d019d14..e67b715a 100644 --- a/ext/js/templates/template-renderer-proxy.js +++ b/ext/js/templates/template-renderer-proxy.js @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import {deserializeError, generateId} from '../core.js'; +import {generateId} from '../core.js'; export class TemplateRendererProxy { constructor() { -- cgit v1.2.3 From 6a3dae04de68ab633da15bbc8ec6b350e38e6d2f Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Nov 2023 13:54:43 -0500 Subject: Add extension error imports --- ext/js/app/popup.js | 1 + ext/js/comm/anki-connect.js | 1 + ext/js/data/anki-note-builder.js | 1 + ext/js/display/display-generator.js | 1 + ext/js/display/option-toggle-hotkey-handler.js | 1 + ext/js/language/dictionary-worker-handler.js | 1 + ext/js/language/dictionary-worker-media-loader.js | 1 + ext/js/language/dictionary-worker.js | 1 + ext/js/media/audio-downloader.js | 1 + ext/js/pages/settings/anki-controller.js | 1 + ext/js/pages/settings/anki-templates-controller.js | 1 + ext/js/pages/settings/dictionary-import-controller.js | 1 + ext/js/pages/settings/generic-setting-controller.js | 1 + ext/js/templates/sandbox/template-renderer-frame-api.js | 2 ++ ext/js/templates/sandbox/template-renderer.js | 1 + ext/js/templates/template-renderer-proxy.js | 1 + 16 files changed, 17 insertions(+) (limited to 'ext/js/comm') diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index 31b18f01..4f201fc3 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -18,6 +18,7 @@ import {FrameClient} from '../comm/frame-client.js'; import {DynamicProperty, EventDispatcher, EventListenerCollection, deepEqual} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js'; import {DocumentUtil} from '../dom/document-util.js'; import {dynamicLoader} from '../script/dynamic-loader.js'; import {yomitan} from '../yomitan.js'; diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index b876703f..7ff8d0e1 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ExtensionError} from '../core/extension-error.js'; import {AnkiUtil} from '../data/anki-util.js'; /** diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index f8296ee0..28809e1f 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -17,6 +17,7 @@ */ import {deferPromise} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js'; import {TemplateRendererProxy} from '../templates/template-renderer-proxy.js'; import {yomitan} from '../yomitan.js'; import {AnkiUtil} from './anki-util.js'; diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index 9fc700f3..e8be79d9 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -17,6 +17,7 @@ */ import {isObject} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js'; import {HtmlTemplateCollection} from '../dom/html-template-collection.js'; import {DictionaryDataUtil} from '../language/sandbox/dictionary-data-util.js'; import {yomitan} from '../yomitan.js'; diff --git a/ext/js/display/option-toggle-hotkey-handler.js b/ext/js/display/option-toggle-hotkey-handler.js index e73fcf04..9677e86b 100644 --- a/ext/js/display/option-toggle-hotkey-handler.js +++ b/ext/js/display/option-toggle-hotkey-handler.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ExtensionError} from '../core/extension-error.js'; import {yomitan} from '../yomitan.js'; export class OptionToggleHotkeyHandler { diff --git a/ext/js/language/dictionary-worker-handler.js b/ext/js/language/dictionary-worker-handler.js index 3a85cb71..9a724386 100644 --- a/ext/js/language/dictionary-worker-handler.js +++ b/ext/js/language/dictionary-worker-handler.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ExtensionError} from '../core/extension-error.js'; import {DictionaryDatabase} from './dictionary-database.js'; import {DictionaryImporter} from './dictionary-importer.js'; import {DictionaryWorkerMediaLoader} from './dictionary-worker-media-loader.js'; diff --git a/ext/js/language/dictionary-worker-media-loader.js b/ext/js/language/dictionary-worker-media-loader.js index 2701389e..e19a13d3 100644 --- a/ext/js/language/dictionary-worker-media-loader.js +++ b/ext/js/language/dictionary-worker-media-loader.js @@ -17,6 +17,7 @@ */ import {generateId} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js'; /** * Class used for loading and validating media from a worker thread diff --git a/ext/js/language/dictionary-worker.js b/ext/js/language/dictionary-worker.js index b9d0236c..3e78a6ff 100644 --- a/ext/js/language/dictionary-worker.js +++ b/ext/js/language/dictionary-worker.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ExtensionError} from '../core/extension-error.js'; import {DictionaryImporterMediaLoader} from './dictionary-importer-media-loader.js'; export class DictionaryWorker { diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index 8bd04b2b..7b236790 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -17,6 +17,7 @@ */ import {RequestBuilder} from '../background/request-builder.js'; +import {ExtensionError} from '../core/extension-error.js'; import {JsonSchema} from '../data/json-schema.js'; import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; import {NativeSimpleDOMParser} from '../dom/native-simple-dom-parser.js'; diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js index 0ccd018d..722459df 100644 --- a/ext/js/pages/settings/anki-controller.js +++ b/ext/js/pages/settings/anki-controller.js @@ -18,6 +18,7 @@ import {AnkiConnect} from '../../comm/anki-connect.js'; import {EventListenerCollection, log} from '../../core.js'; +import {ExtensionError} from '../../core/extension-error.js'; import {AnkiUtil} from '../../data/anki-util.js'; import {SelectorObserver} from '../../dom/selector-observer.js'; import {ObjectPropertyAccessor} from '../../general/object-property-accessor.js'; diff --git a/ext/js/pages/settings/anki-templates-controller.js b/ext/js/pages/settings/anki-templates-controller.js index d2814880..a0ff96b2 100644 --- a/ext/js/pages/settings/anki-templates-controller.js +++ b/ext/js/pages/settings/anki-templates-controller.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ExtensionError} from '../../core/extension-error.js'; import {AnkiNoteBuilder} from '../../data/anki-note-builder.js'; import {JapaneseUtil} from '../../language/sandbox/japanese-util.js'; import {yomitan} from '../../yomitan.js'; diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js index 106ecbca..af8c2fcd 100644 --- a/ext/js/pages/settings/dictionary-import-controller.js +++ b/ext/js/pages/settings/dictionary-import-controller.js @@ -17,6 +17,7 @@ */ import {log} from '../../core.js'; +import {ExtensionError} from '../../core/extension-error.js'; import {DictionaryWorker} from '../../language/dictionary-worker.js'; import {yomitan} from '../../yomitan.js'; import {DictionaryController} from './dictionary-controller.js'; diff --git a/ext/js/pages/settings/generic-setting-controller.js b/ext/js/pages/settings/generic-setting-controller.js index 3c6104a9..47c0d6fe 100644 --- a/ext/js/pages/settings/generic-setting-controller.js +++ b/ext/js/pages/settings/generic-setting-controller.js @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +import {ExtensionError} from '../../core/extension-error.js'; import {DocumentUtil} from '../../dom/document-util.js'; import {DOMDataBinder} from '../../dom/dom-data-binder.js'; diff --git a/ext/js/templates/sandbox/template-renderer-frame-api.js b/ext/js/templates/sandbox/template-renderer-frame-api.js index 31ba4500..91400ab8 100644 --- a/ext/js/templates/sandbox/template-renderer-frame-api.js +++ b/ext/js/templates/sandbox/template-renderer-frame-api.js @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import {ExtensionError} from '../../core/extension-error.js'; + export class TemplateRendererFrameApi { /** * @param {TemplateRenderer} templateRenderer diff --git a/ext/js/templates/sandbox/template-renderer.js b/ext/js/templates/sandbox/template-renderer.js index c7613533..716e3ccc 100644 --- a/ext/js/templates/sandbox/template-renderer.js +++ b/ext/js/templates/sandbox/template-renderer.js @@ -17,6 +17,7 @@ */ import {Handlebars} from '../../../lib/handlebars.js'; +import {ExtensionError} from '../../core/extension-error.js'; export class TemplateRenderer { constructor() { diff --git a/ext/js/templates/template-renderer-proxy.js b/ext/js/templates/template-renderer-proxy.js index e67b715a..642eea8b 100644 --- a/ext/js/templates/template-renderer-proxy.js +++ b/ext/js/templates/template-renderer-proxy.js @@ -17,6 +17,7 @@ */ import {generateId} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js'; export class TemplateRendererProxy { constructor() { -- cgit v1.2.3 From 7aed9a371b0d74c0d75179a08068e8935b76d780 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Nov 2023 14:55:27 -0500 Subject: Update types --- ext/js/background/backend.js | 22 ++++----- ext/js/background/offscreen-proxy.js | 27 +++++++++-- ext/js/background/offscreen.js | 9 +++- ext/js/background/request-builder.js | 13 ++---- ext/js/comm/api.js | 6 +-- ext/js/comm/clipboard-reader.js | 4 +- ext/js/display/search-action-popup-controller.js | 4 +- ext/js/dom/sandbox/css-style-applier.js | 2 +- ext/js/dom/text-source-element.js | 2 +- ext/js/dom/text-source-range.js | 2 +- ext/js/general/regex-util.js | 2 +- .../__mocks__/dictionary-importer-media-loader.js | 1 + ext/js/language/dictionary-importer.js | 2 +- ext/js/language/dictionary-worker.js | 2 + ext/js/language/sandbox/japanese-util.js | 8 ++-- ext/js/language/text-scanner.js | 1 + ext/js/language/translator.js | 4 +- ext/js/media/audio-downloader.js | 6 +-- ext/js/pages/settings/backup-controller.js | 54 +++++++++------------- .../settings/recommended-permissions-controller.js | 36 +++++++++++++-- types/ext/api.d.ts | 12 +++++ types/ext/request-builder.d.ts | 2 + 22 files changed, 139 insertions(+), 82 deletions(-) (limited to 'ext/js/comm') diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 14877cf1..be68ecf4 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -96,7 +96,7 @@ export class Backend { }); /** @type {?import('settings').Options} */ this._options = null; - /** @type {JsonSchema[]} */ + /** @type {import('../data/json-schema.js').JsonSchema[]} */ this._profileConditionsSchemaCache = []; /** @type {ProfileConditionsUtil} */ this._profileConditionsUtil = new ProfileConditionsUtil(); @@ -665,7 +665,7 @@ export class Backend { async _onApiInjectStylesheet({type, value}, sender) { const {frameId, tab} = sender; if (typeof tab !== 'object' || tab === null || typeof tab.id !== 'number') { throw new Error('Invalid tab'); } - return await this._scriptManager.injectStylesheet(type, value, tab.id, frameId, false, true, 'document_start'); + return await this._scriptManager.injectStylesheet(type, value, tab.id, frameId, false); } /** @type {import('api').Handler} */ @@ -895,13 +895,7 @@ export class Backend { } } - /** - * - * @param root0 - * @param root0.targetTabId - * @param root0.targetFrameId - * @param sender - */ + /** @type {import('api').Handler} */ _onApiOpenCrossFramePort({targetTabId, targetFrameId}, sender) { const sourceTabId = (sender && sender.tab ? sender.tab.id : null); if (typeof sourceTabId !== 'number') { @@ -922,7 +916,9 @@ export class Backend { otherTabId: sourceTabId, otherFrameId: sourceFrameId }; + /** @type {?chrome.runtime.Port} */ let sourcePort = chrome.tabs.connect(sourceTabId, {frameId: sourceFrameId, name: JSON.stringify(sourceDetails)}); + /** @type {?chrome.runtime.Port} */ let targetPort = chrome.tabs.connect(targetTabId, {frameId: targetFrameId, name: JSON.stringify(targetDetails)}); const cleanup = () => { @@ -937,8 +933,12 @@ export class Backend { } }; - sourcePort.onMessage.addListener((message) => { targetPort.postMessage(message); }); - targetPort.onMessage.addListener((message) => { sourcePort.postMessage(message); }); + sourcePort.onMessage.addListener((message) => { + if (targetPort !== null) { targetPort.postMessage(message); } + }); + targetPort.onMessage.addListener((message) => { + if (sourcePort !== null) { sourcePort.postMessage(message); } + }); sourcePort.onDisconnect.addListener(cleanup); targetPort.onDisconnect.addListener(cleanup); diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js index c01f523d..0fb2f269 100644 --- a/ext/js/background/offscreen-proxy.js +++ b/ext/js/background/offscreen-proxy.js @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import {deserializeError, isObject} from '../core.js'; +import {isObject} from '../core.js'; import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; export class OffscreenProxy { @@ -158,15 +158,36 @@ export class TranslatorProxy { } export class ClipboardReaderProxy { + /** + * @param {OffscreenProxy} offscreen + */ constructor(offscreen) { + /** @type {?import('environment').Browser} */ + this._browser = null; + /** @type {OffscreenProxy} */ this._offscreen = offscreen; } + /** @type {?import('environment').Browser} */ + get browser() { return this._browser; } + set browser(value) { + if (this._browser === value) { return; } + this._browser = value; + this._offscreen.sendMessagePromise({action: 'clipboardSetBrowserOffsecreen', params: {value}}); + } + + /** + * @param {boolean} useRichText + * @returns {Promise} + */ async getText(useRichText) { - return this._offscreen.sendMessagePromise({action: 'clipboardGetTextOffscreen', params: {useRichText}}); + return await this._offscreen.sendMessagePromise({action: 'clipboardGetTextOffscreen', params: {useRichText}}); } + /** + * @returns {Promise} + */ async getImage() { - return this._offscreen.sendMessagePromise({action: 'clipboardGetImageOffscreen'}); + return await this._offscreen.sendMessagePromise({action: 'clipboardGetImageOffscreen'}); } } diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js index 27cee8c4..6302aa84 100644 --- a/ext/js/background/offscreen.js +++ b/ext/js/background/offscreen.js @@ -50,6 +50,7 @@ export class Offscreen { 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)}], @@ -59,7 +60,6 @@ export class Offscreen { ['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)}] - ]); const onMessage = this._onMessage.bind(this); @@ -76,6 +76,13 @@ export class Offscreen { return this._clipboardReader.getImage(); } + /** + * @param {{value: import('environment').Browser}} details + */ + _setClipboardBrowser({value}) { + this._clipboardReader.browser = value; + } + _prepareDatabaseHandler() { if (this._prepareDatabasePromise !== null) { return this._prepareDatabasePromise; diff --git a/ext/js/background/request-builder.js b/ext/js/background/request-builder.js index 48fe2dd9..5ae7fbf5 100644 --- a/ext/js/background/request-builder.js +++ b/ext/js/background/request-builder.js @@ -21,12 +21,6 @@ * with additional controls over anonymity and error handling. */ export class RequestBuilder { - /** - * A progress callback for a fetch read. - * @callback ProgressCallback - * @param {boolean} complete Whether or not the data has been completely read. - */ - /** * Creates a new instance. */ @@ -109,14 +103,17 @@ export class RequestBuilder { /** * Reads the array buffer body of a fetch response, with an optional `onProgress` callback. * @param {Response} response The response of a `fetch` call. - * @param {ProgressCallback} onProgress The progress callback + * @param {?import('request-builder.js').ProgressCallback} onProgress The progress callback * @returns {Promise} The resulting binary data. */ static async readFetchResponseArrayBuffer(response, onProgress) { let reader; try { if (typeof onProgress === 'function') { - reader = response.body.getReader(); + const {body} = response; + if (body !== null) { + reader = body.getReader(); + } } } catch (e) { // Not supported diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 62dc98b1..0cfdba59 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -415,9 +415,9 @@ export class API { } /** - * - * @param targetTabId - * @param targetFrameId + * @param {import('api').OpenCrossFramePortDetails['targetTabId']} targetTabId + * @param {import('api').OpenCrossFramePortDetails['targetFrameId']} targetFrameId + * @returns {Promise} */ openCrossFramePort(targetTabId, targetFrameId) { return this._invoke('openCrossFramePort', {targetTabId, targetFrameId}); diff --git a/ext/js/comm/clipboard-reader.js b/ext/js/comm/clipboard-reader.js index c7b45a7c..364e31a3 100644 --- a/ext/js/comm/clipboard-reader.js +++ b/ext/js/comm/clipboard-reader.js @@ -29,7 +29,7 @@ export class ClipboardReader { constructor({document=null, pasteTargetSelector=null, richContentPasteTargetSelector=null}) { /** @type {?Document} */ this._document = document; - /** @type {?string} */ + /** @type {?import('environment').Browser} */ this._browser = null; /** @type {?HTMLTextAreaElement} */ this._pasteTarget = null; @@ -43,7 +43,7 @@ export class ClipboardReader { /** * Gets the browser being used. - * @type {?string} + * @type {?import('environment').Browser} */ get browser() { return this._browser; diff --git a/ext/js/display/search-action-popup-controller.js b/ext/js/display/search-action-popup-controller.js index 733fd70a..3a2057a1 100644 --- a/ext/js/display/search-action-popup-controller.js +++ b/ext/js/display/search-action-popup-controller.js @@ -18,10 +18,10 @@ export class SearchActionPopupController { /** - * @param {SearchPersistentStateController} searchPersistentStateController + * @param {import('./search-persistent-state-controller.js').SearchPersistentStateController} searchPersistentStateController */ constructor(searchPersistentStateController) { - /** @type {SearchPersistentStateController} */ + /** @type {import('./search-persistent-state-controller.js').SearchPersistentStateController} */ this._searchPersistentStateController = searchPersistentStateController; } diff --git a/ext/js/dom/sandbox/css-style-applier.js b/ext/js/dom/sandbox/css-style-applier.js index 332ca4f2..ea36a02d 100644 --- a/ext/js/dom/sandbox/css-style-applier.js +++ b/ext/js/dom/sandbox/css-style-applier.js @@ -24,7 +24,7 @@ export class CssStyleApplier { /** * Creates a new instance of the class. * @param {string} styleDataUrl The local URL to the JSON file continaing the style rules. - * The style rules should follow the format of {@link CssStyleApplierRawStyleData}. + * The style rules should follow the format of `CssStyleApplierRawStyleData`. */ constructor(styleDataUrl) { /** @type {string} */ diff --git a/ext/js/dom/text-source-element.js b/ext/js/dom/text-source-element.js index 47c18e30..40ff5cc9 100644 --- a/ext/js/dom/text-source-element.js +++ b/ext/js/dom/text-source-element.js @@ -173,7 +173,7 @@ export class TextSourceElement { /** * Checks whether another text source has the same starting point. - * @param {TextSourceElement|TextSourceRange} other The other source to test. + * @param {import('text-source').TextSource} other The other source to test. * @returns {boolean} `true` if the starting points are equivalent, `false` otherwise. */ hasSameStart(other) { diff --git a/ext/js/dom/text-source-range.js b/ext/js/dom/text-source-range.js index 5dbbd636..fd09fdda 100644 --- a/ext/js/dom/text-source-range.js +++ b/ext/js/dom/text-source-range.js @@ -206,7 +206,7 @@ export class TextSourceRange { /** * Checks whether another text source has the same starting point. - * @param {TextSourceElement|TextSourceRange} other The other source to test. + * @param {import('text-source').TextSource} other The other source to test. * @returns {boolean} `true` if the starting points are equivalent, `false` otherwise. * @throws {Error} An exception can be thrown if `Range.compareBoundaryPoints` fails, * which shouldn't happen, but the handler is kept in case of unexpected errors. diff --git a/ext/js/general/regex-util.js b/ext/js/general/regex-util.js index 726ce9f2..62248968 100644 --- a/ext/js/general/regex-util.js +++ b/ext/js/general/regex-util.js @@ -25,7 +25,7 @@ export class RegexUtil { * Applies string.replace using a regular expression and replacement string as arguments. * A source map of the changes is also maintained. * @param {string} text A string of the text to replace. - * @param {TextSourceMap} sourceMap An instance of `TextSourceMap` which corresponds to `text`. + * @param {import('./text-source-map.js').TextSourceMap} sourceMap An instance of `TextSourceMap` which corresponds to `text`. * @param {RegExp} pattern A regular expression to use as the replacement. * @param {string} replacement A replacement string that follows the format of the standard * JavaScript regular expression replacement string. diff --git a/ext/js/language/__mocks__/dictionary-importer-media-loader.js b/ext/js/language/__mocks__/dictionary-importer-media-loader.js index 96f0f6dd..ffda29b3 100644 --- a/ext/js/language/__mocks__/dictionary-importer-media-loader.js +++ b/ext/js/language/__mocks__/dictionary-importer-media-loader.js @@ -17,6 +17,7 @@ */ export class DictionaryImporterMediaLoader { + /** @type {import('dictionary-importer-media-loader').GetImageDetailsFunction} */ async getImageDetails(content) { // Placeholder values return {content, width: 100, height: 100}; diff --git a/ext/js/language/dictionary-importer.js b/ext/js/language/dictionary-importer.js index aa6d7ae6..2a2f4063 100644 --- a/ext/js/language/dictionary-importer.js +++ b/ext/js/language/dictionary-importer.js @@ -36,7 +36,7 @@ export class DictionaryImporter { } /** - * @param {DictionaryDatabase} dictionaryDatabase + * @param {import('./dictionary-database.js').DictionaryDatabase} dictionaryDatabase * @param {ArrayBuffer} archiveContent * @param {import('dictionary-importer').ImportDetails} details * @returns {Promise} diff --git a/ext/js/language/dictionary-worker.js b/ext/js/language/dictionary-worker.js index 3e78a6ff..3119dd7b 100644 --- a/ext/js/language/dictionary-worker.js +++ b/ext/js/language/dictionary-worker.js @@ -157,6 +157,8 @@ export class DictionaryWorker { resolve(result2); } else { // If formatResult is not provided, the response is assumed to be the same type + // For some reason, eslint thinks the TResponse type is undefined + // eslint-disable-next-line jsdoc/no-undefined-types resolve(/** @type {TResponse} */ (/** @type {unknown} */ (result))); } } diff --git a/ext/js/language/sandbox/japanese-util.js b/ext/js/language/sandbox/japanese-util.js index f7f20b3b..4c9c46bd 100644 --- a/ext/js/language/sandbox/japanese-util.js +++ b/ext/js/language/sandbox/japanese-util.js @@ -466,7 +466,7 @@ export class JapaneseUtil { /** * @param {string} text - * @param {?TextSourceMap} [sourceMap] + * @param {?import('../../general/text-source-map.js').TextSourceMap} [sourceMap] * @returns {string} */ convertHalfWidthKanaToFullWidth(text, sourceMap=null) { @@ -513,7 +513,7 @@ export class JapaneseUtil { /** * @param {string} text - * @param {?TextSourceMap} sourceMap + * @param {?import('../../general/text-source-map.js').TextSourceMap} sourceMap * @returns {string} */ convertAlphabeticToKana(text, sourceMap=null) { @@ -676,7 +676,7 @@ export class JapaneseUtil { /** * @param {string} text * @param {boolean} fullCollapse - * @param {?TextSourceMap} [sourceMap] + * @param {?import('../../general/text-source-map.js').TextSourceMap} [sourceMap] * @returns {string} */ collapseEmphaticSequences(text, fullCollapse, sourceMap=null) { @@ -816,7 +816,7 @@ export class JapaneseUtil { /** * @param {string} text - * @param {?TextSourceMap} sourceMap + * @param {?import('../../general/text-source-map.js').TextSourceMap} sourceMap * @param {number} sourceMapStart * @returns {string} */ diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js index b4d9a642..f6bcde8d 100644 --- a/ext/js/language/text-scanner.js +++ b/ext/js/language/text-scanner.js @@ -18,6 +18,7 @@ import {EventDispatcher, EventListenerCollection, clone, log} from '../core.js'; import {DocumentUtil} from '../dom/document-util.js'; +import {TextSourceElement} from '../dom/text-source-element.js'; import {yomitan} from '../yomitan.js'; /** diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 67cc53c6..c21b16b1 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -29,9 +29,9 @@ export class Translator { * @param {import('translator').ConstructorDetails} details The details for the class. */ constructor({japaneseUtil, database}) { - /** @type {JapaneseUtil} */ + /** @type {import('./sandbox/japanese-util.js').JapaneseUtil} */ this._japaneseUtil = japaneseUtil; - /** @type {DictionaryDatabase} */ + /** @type {import('./dictionary-database.js').DictionaryDatabase} */ this._database = database; /** @type {?Deinflector} */ this._deinflector = null; diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index 7b236790..0847d479 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -25,10 +25,10 @@ import {SimpleDOMParser} from '../dom/simple-dom-parser.js'; export class AudioDownloader { /** - * @param {{japaneseUtil: JapaneseUtil, requestBuilder: RequestBuilder}} details + * @param {{japaneseUtil: import('../language/sandbox/japanese-util.js').JapaneseUtil, requestBuilder: RequestBuilder}} details */ constructor({japaneseUtil, requestBuilder}) { - /** @type {JapaneseUtil} */ + /** @type {import('../language/sandbox/japanese-util.js').JapaneseUtil} */ this._japaneseUtil = japaneseUtil; /** @type {RequestBuilder} */ this._requestBuilder = requestBuilder; @@ -314,7 +314,7 @@ export class AudioDownloader { */ async _downloadAudioFromUrl(url, sourceType, idleTimeout) { let signal; - /** @type {?(done: boolean) => void} */ + /** @type {?import('request-builder.js').ProgressCallback} */ let onProgress = null; /** @type {?number} */ let idleTimer = null; diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js index 50a50b1a..52c5f418 100644 --- a/ext/js/pages/settings/backup-controller.js +++ b/ext/js/pages/settings/backup-controller.js @@ -534,12 +534,11 @@ export class BackupController { // Exporting Dictionaries Database /** - * - * @param message - * @param isWarning + * @param {string} message + * @param {boolean} [isWarning] */ _databaseExportImportErrorMessage(message, isWarning=false) { - const errorMessageContainer = document.querySelector('#db-ops-error-report'); + const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report')); errorMessageContainer.style.display = 'block'; errorMessageContainer.textContent = message; @@ -553,15 +552,11 @@ export class BackupController { } /** - * - * @param root0 - * @param root0.totalRows - * @param root0.completedRows - * @param root0.done + * @param {{totalRows: number, completedRows: number, done: boolean}} details */ _databaseExportProgressCallback({totalRows, completedRows, done}) { console.log(`Progress: ${completedRows} of ${totalRows} rows completed`); - const messageContainer = document.querySelector('#db-ops-progress-report'); + const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report')); messageContainer.style.display = 'block'; messageContainer.textContent = `Export Progress: ${completedRows} of ${totalRows} rows completed`; @@ -572,8 +567,8 @@ export class BackupController { } /** - * - * @param databaseName + * @param {string} databaseName + * @returns {Promise} */ async _exportDatabase(databaseName) { const db = await new Dexie(databaseName).open(); @@ -592,7 +587,7 @@ export class BackupController { return; } - const errorMessageContainer = document.querySelector('#db-ops-error-report'); + const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report')); errorMessageContainer.style.display = 'none'; const date = new Date(Date.now()); @@ -616,15 +611,11 @@ export class BackupController { // Importing Dictionaries Database /** - * - * @param root0 - * @param root0.totalRows - * @param root0.completedRows - * @param root0.done + * @param {{totalRows: number, completedRows: number, done: boolean}} details */ _databaseImportProgressCallback({totalRows, completedRows, done}) { console.log(`Progress: ${completedRows} of ${totalRows} rows completed`); - const messageContainer = document.querySelector('#db-ops-progress-report'); + const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report')); messageContainer.style.display = 'block'; messageContainer.style.color = '#4169e1'; messageContainer.textContent = `Import Progress: ${completedRows} of ${totalRows} rows completed`; @@ -637,9 +628,8 @@ export class BackupController { } /** - * - * @param databaseName - * @param file + * @param {string} databaseName + * @param {File} file */ async _importDatabase(databaseName, file) { await yomitan.api.purgeDatabase(); @@ -648,16 +638,13 @@ export class BackupController { yomitan.trigger('storageChanged'); } - /** - * - */ + /** */ _onSettingsImportDatabaseClick() { - document.querySelector('#settings-import-db').click(); + /** @type {HTMLElement} */ (document.querySelector('#settings-import-db')).click(); } /** - * - * @param e + * @param {Event} e */ async _onSettingsImportDatabaseChange(e) { if (this._settingsExportDatabaseToken !== null) { @@ -666,22 +653,23 @@ export class BackupController { return; } - const errorMessageContainer = document.querySelector('#db-ops-error-report'); + const errorMessageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-error-report')); errorMessageContainer.style.display = 'none'; - const files = e.target.files; - if (files.length === 0) { return; } + const element = /** @type {HTMLInputElement} */ (e.currentTarget); + const files = element.files; + if (files === null || files.length === 0) { return; } const pageExitPrevention = this._settingsController.preventPageExit(); const file = files[0]; - e.target.value = null; + element.value = ''; try { const token = {}; this._settingsExportDatabaseToken = token; await this._importDatabase(this._dictionariesDatabaseName, file); } catch (error) { console.log(error); - const messageContainer = document.querySelector('#db-ops-progress-report'); + const messageContainer = /** @type {HTMLElement} */ (document.querySelector('#db-ops-progress-report')); messageContainer.style.color = 'red'; this._databaseExportImportErrorMessage('Encountered errors when importing. Please restart the browser and try again. If it continues to fail, reinstall Yomitan and import dictionaries one-by-one.'); } finally { diff --git a/ext/js/pages/settings/recommended-permissions-controller.js b/ext/js/pages/settings/recommended-permissions-controller.js index e04dbdf7..b19311aa 100644 --- a/ext/js/pages/settings/recommended-permissions-controller.js +++ b/ext/js/pages/settings/recommended-permissions-controller.js @@ -19,13 +19,21 @@ import {EventListenerCollection} from '../../core.js'; export class RecommendedPermissionsController { + /** + * @param {import('./settings-controller.js').SettingsController} settingsController + */ constructor(settingsController) { + /** @type {import('./settings-controller.js').SettingsController} */ this._settingsController = settingsController; + /** @type {?NodeListOf} */ this._originToggleNodes = null; + /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); + /** @type {?HTMLElement} */ this._errorContainer = null; } + /** */ async prepare() { this._originToggleNodes = document.querySelectorAll('.recommended-permissions-toggle'); this._errorContainer = document.querySelector('#recommended-permissions-error'); @@ -39,35 +47,53 @@ export class RecommendedPermissionsController { // Private + /** + * @param {import('settings-controller').PermissionsChangedEvent} details + */ _onPermissionsChanged({permissions}) { this._eventListeners.removeAllEventListeners(); const originsSet = new Set(permissions.origins); - for (const node of this._originToggleNodes) { - node.checked = originsSet.has(node.dataset.origin); + if (this._originToggleNodes !== null) { + for (const node of this._originToggleNodes) { + const {origin} = node.dataset; + node.checked = typeof origin === 'string' && originsSet.has(origin); + } } } + /** + * @param {Event} e + */ _onOriginToggleChange(e) { - const node = e.currentTarget; + const node = /** @type {HTMLInputElement} */ (e.currentTarget); const value = node.checked; node.checked = !value; const {origin} = node.dataset; + if (typeof origin !== 'string') { return; } this._setOriginPermissionEnabled(origin, value); } + /** */ async _updatePermissions() { const permissions = await this._settingsController.permissionsUtil.getAllPermissions(); this._onPermissionsChanged({permissions}); } + /** + * @param {string} origin + * @param {boolean} enabled + * @returns {Promise} + */ async _setOriginPermissionEnabled(origin, enabled) { let added = false; try { added = await this._settingsController.permissionsUtil.setPermissionsGranted({origins: [origin]}, enabled); } catch (e) { - this._errorContainer.hidden = false; - this._errorContainer.textContent = e.message; + if (this._errorContainer !== null) { + this._errorContainer.hidden = false; + this._errorContainer.textContent = e instanceof Error ? e.message : `${e}`; + } } if (!added) { return false; } await this._updatePermissions(); diff --git a/types/ext/api.d.ts b/types/ext/api.d.ts index 19a62c1c..6b7b4b19 100644 --- a/types/ext/api.d.ts +++ b/types/ext/api.d.ts @@ -444,6 +444,18 @@ export type LoadExtensionScriptsDetails = { export type LoadExtensionScriptsResult = void; +// openCrossFramePort + +export type OpenCrossFramePortDetails = { + targetTabId: number; + targetFrameId: number; +}; + +export type OpenCrossFramePortResult = { + targetTabId: number; + targetFrameId: number; +}; + // requestBackendReadySignal export type RequestBackendReadySignalDetails = Record; diff --git a/types/ext/request-builder.d.ts b/types/ext/request-builder.d.ts index 0acf5ede..8f375754 100644 --- a/types/ext/request-builder.d.ts +++ b/types/ext/request-builder.d.ts @@ -19,3 +19,5 @@ export type FetchEventListeners = { onBeforeSendHeaders: ((details: chrome.webRequest.WebRequestHeadersDetails) => (chrome.webRequest.BlockingResponse | void)) | null; onErrorOccurred: ((details: chrome.webRequest.WebResponseErrorDetails) => void) | null; }; + +export type ProgressCallback = (complete: boolean) => void; -- cgit v1.2.3 From 14d12f6ba20b837a04c638b935773f3120e194ff Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Nov 2023 19:33:01 -0500 Subject: Update timer types and such --- dev/jsconfig.json | 2 +- ext/js/app/frontend.js | 11 +++++++++-- ext/js/background/backend.js | 4 ++-- ext/js/background/offscreen.js | 4 +++- ext/js/comm/api.js | 2 +- ext/js/comm/clipboard-monitor.js | 2 +- ext/js/comm/cross-frame-api.js | 21 +++++++++++++-------- ext/js/comm/frame-ancestry-handler.js | 2 +- ext/js/comm/frame-client.js | 2 +- ext/js/comm/mecab.js | 2 +- ext/js/core.js | 2 +- ext/js/display/display-audio.js | 2 +- ext/js/display/display-notification.js | 2 +- ext/js/display/display.js | 2 +- ext/js/display/element-overflow-controller.js | 8 ++++---- ext/js/display/option-toggle-hotkey-handler.js | 2 +- ext/js/display/search-display-controller.js | 2 +- ext/js/dom/document-util.js | 2 +- ext/js/dom/panel-element.js | 2 +- ext/js/language/text-scanner.js | 6 +++--- ext/js/media/audio-downloader.js | 2 +- ext/js/pages/settings/popup-preview-frame.js | 2 +- test/jsconfig.json | 2 +- types/ext/core.d.ts | 2 ++ types/ext/cross-frame-api.d.ts | 2 +- types/ext/offscreen.d.ts | 2 +- types/other/globals.d.ts | 22 ++++++++++++++++++++++ types/other/web-set-timeout.d.ts | 24 ------------------------ 28 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 types/other/globals.d.ts delete mode 100644 types/other/web-set-timeout.d.ts (limited to 'ext/js/comm') diff --git a/dev/jsconfig.json b/dev/jsconfig.json index 518f97ad..d4efe694 100644 --- a/dev/jsconfig.json +++ b/dev/jsconfig.json @@ -71,7 +71,7 @@ "../ext/js/language/translator.js", "../ext/js/media/media-util.js", "../types/dev/**/*.ts", - "../types/other/web-set-timeout.d.ts" + "../types/other/globals.d.ts" ], "exclude": [ "../node_modules" diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index 628c504e..e1f8d8c9 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -99,7 +99,7 @@ export class Frontend { this._popupEventListeners = new EventListenerCollection(); /** @type {?import('core').TokenObject} */ this._updatePopupToken = null; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._clearSelectionTimer = null; /** @type {boolean} */ this._isPointerOverPopup = false; @@ -840,7 +840,7 @@ export class Frontend { */ async _waitForFrontendReady(frameId, timeout) { return new Promise((resolve, reject) => { - /** @type {?number} */ + /** @type {?import('core').Timeout} */ let timeoutId = null; const cleanup = () => { @@ -973,6 +973,13 @@ export class Frontend { await yomitan.api.loadExtensionScripts([ '/js/accessibility/google-docs-util.js' ]); + this._prepareGoogleDocs2(); + } + + /** + * @returns {Promise} + */ + async _prepareGoogleDocs2() { if (typeof GoogleDocsUtil === 'undefined') { return; } DocumentUtil.registerGetRangeFromPointHandler(GoogleDocsUtil.getRangeFromPoint.bind(GoogleDocsUtil)); } diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 44f5a42d..f1a47e7f 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -138,7 +138,7 @@ export class Backend { /** @type {?string} */ this._defaultBrowserActionTitle = null; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._badgePrepareDelayTimer = null; /** @type {?import('log').LogLevel} */ this._logErrorLevel = null; @@ -1981,7 +1981,7 @@ export class Backend { */ _waitUntilTabFrameIsReady(tabId, frameId, timeout=null) { return new Promise((resolve, reject) => { - /** @type {?number} */ + /** @type {?import('core').Timeout} */ let timer = null; /** @type {?import('extension').ChromeRuntimeOnMessageCallback} */ let onMessage = (message, sender) => { diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js index 45345c01..8da661ad 100644 --- a/ext/js/background/offscreen.js +++ b/ext/js/background/offscreen.js @@ -51,7 +51,7 @@ export class Offscreen { }); /** @type {import('offscreen').MessageHandlerMap} */ - this._messageHandlers = new Map([ + const messageHandlers = new Map([ ['clipboardGetTextOffscreen', {async: true, handler: this._getTextHandler.bind(this)}], ['clipboardGetImageOffscreen', {async: true, handler: this._getImageHandler.bind(this)}], ['clipboardSetBrowserOffscreen', {async: false, handler: this._setClipboardBrowser.bind(this)}], @@ -65,6 +65,8 @@ export class Offscreen { ['getTermFrequenciesOffscreen', {async: true, handler: this._getTermFrequenciesHandler.bind(this)}], ['clearDatabaseCachesOffscreen', {async: false, handler: this._clearDatabaseCachesHandler.bind(this)}] ]); + /** @type {import('offscreen').MessageHandlerMap} */ + this._messageHandlers = messageHandlers; const onMessage = this._onMessage.bind(this); chrome.runtime.onMessage.addListener(onMessage); diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 0cfdba59..26218595 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -431,7 +431,7 @@ export class API { */ _createActionPort(timeout) { return new Promise((resolve, reject) => { - /** @type {?number} */ + /** @type {?import('core').Timeout} */ let timer = null; const portDetails = deferPromise(); diff --git a/ext/js/comm/clipboard-monitor.js b/ext/js/comm/clipboard-monitor.js index 06e95438..3b3a56a9 100644 --- a/ext/js/comm/clipboard-monitor.js +++ b/ext/js/comm/clipboard-monitor.js @@ -31,7 +31,7 @@ export class ClipboardMonitor extends EventDispatcher { this._japaneseUtil = japaneseUtil; /** @type {import('clipboard-monitor').ClipboardReaderLike} */ this._clipboardReader = clipboardReader; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._timerId = null; /** @type {?import('core').TokenObject} */ this._timerToken = null; diff --git a/ext/js/comm/cross-frame-api.js b/ext/js/comm/cross-frame-api.js index 34f3f36a..3ac38cf2 100644 --- a/ext/js/comm/cross-frame-api.js +++ b/ext/js/comm/cross-frame-api.js @@ -25,14 +25,14 @@ import {yomitan} from '../yomitan.js'; */ class CrossFrameAPIPort extends EventDispatcher { /** - * @param {?number} otherTabId + * @param {number} otherTabId * @param {number} otherFrameId * @param {chrome.runtime.Port} port * @param {import('core').MessageHandlerMap} messageHandlers */ constructor(otherTabId, otherFrameId, port, messageHandlers) { super(); - /** @type {?number} */ + /** @type {number} */ this._otherTabId = otherTabId; /** @type {number} */ this._otherFrameId = otherFrameId; @@ -48,7 +48,7 @@ class CrossFrameAPIPort extends EventDispatcher { this._eventListeners = new EventListenerCollection(); } - /** @type {?number} */ + /** @type {number} */ get otherTabId() { return this._otherTabId; } @@ -299,7 +299,7 @@ export class CrossFrameAPI { this._ackTimeout = 3000; // 3 seconds /** @type {number} */ this._responseTimeout = 10000; // 10 seconds - /** @type {Map>} */ + /** @type {Map>} */ this._commPorts = new Map(); /** @type {import('core').MessageHandlerMap} */ this._messageHandlers = new Map(); @@ -339,7 +339,12 @@ export class CrossFrameAPI { * @returns {Promise} */ async invokeTab(targetTabId, targetFrameId, action, params) { - if (typeof targetTabId !== 'number') { targetTabId = this._tabId; } + if (typeof targetTabId !== 'number') { + targetTabId = this._tabId; + if (typeof targetTabId !== 'number') { + throw new Error('Unknown target tab id for invocation'); + } + } const commPort = await this._getOrCreateCommPort(targetTabId, targetFrameId); return await commPort.invoke(action, params, this._ackTimeout, this._responseTimeout); } @@ -405,7 +410,7 @@ export class CrossFrameAPI { } /** - * @param {?number} otherTabId + * @param {number} otherTabId * @param {number} otherFrameId * @returns {Promise} */ @@ -420,7 +425,7 @@ export class CrossFrameAPI { return await this._createCommPort(otherTabId, otherFrameId); } /** - * @param {?number} otherTabId + * @param {number} otherTabId * @param {number} otherFrameId * @returns {Promise} */ @@ -438,7 +443,7 @@ export class CrossFrameAPI { } /** - * @param {?number} otherTabId + * @param {number} otherTabId * @param {number} otherFrameId * @param {chrome.runtime.Port} port * @returns {CrossFrameAPIPort} diff --git a/ext/js/comm/frame-ancestry-handler.js b/ext/js/comm/frame-ancestry-handler.js index 49c96c22..687ec368 100644 --- a/ext/js/comm/frame-ancestry-handler.js +++ b/ext/js/comm/frame-ancestry-handler.js @@ -122,7 +122,7 @@ export class FrameAncestryHandler { const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`; /** @type {number[]} */ const results = []; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ let timer = null; const cleanup = () => { diff --git a/ext/js/comm/frame-client.js b/ext/js/comm/frame-client.js index 1519cf7f..8aa8c6d6 100644 --- a/ext/js/comm/frame-client.js +++ b/ext/js/comm/frame-client.js @@ -83,7 +83,7 @@ export class FrameClient { _connectInternal(frame, targetOrigin, hostFrameId, setupFrame, timeout) { return new Promise((resolve, reject) => { const tokenMap = new Map(); - /** @type {?number} */ + /** @type {?import('core').Timeout} */ let timer = null; const deferPromiseDetails = /** @type {import('core').DeferredPromiseDetails} */ (deferPromise()); const frameLoadedPromise = deferPromiseDetails.promise; diff --git a/ext/js/comm/mecab.js b/ext/js/comm/mecab.js index 072b42f3..0a87463b 100644 --- a/ext/js/comm/mecab.js +++ b/ext/js/comm/mecab.js @@ -31,7 +31,7 @@ export class Mecab { this._port = null; /** @type {number} */ this._sequence = 0; - /** @type {Map void, reject: (reason?: unknown) => void, timer: number}>} */ + /** @type {Map void, reject: (reason?: unknown) => void, timer: import('core').Timeout}>} */ this._invocations = new Map(); /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); diff --git a/ext/js/core.js b/ext/js/core.js index cdac2020..a53d5572 100644 --- a/ext/js/core.js +++ b/ext/js/core.js @@ -307,7 +307,7 @@ export function promiseAnimationFrame(timeout) { return; } - /** @type {?number} */ + /** @type {?import('core').Timeout} */ let timer = null; /** @type {?number} */ let frameRequest = null; diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js index 3fbfc3c8..3576decb 100644 --- a/ext/js/display/display-audio.js +++ b/ext/js/display/display-audio.js @@ -36,7 +36,7 @@ export class DisplayAudio { this._playbackVolume = 1.0; /** @type {boolean} */ this._autoPlay = false; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._autoPlayAudioTimer = null; /** @type {number} */ this._autoPlayAudioDelay = 400; diff --git a/ext/js/display/display-notification.js b/ext/js/display/display-notification.js index b3f20700..d1cfa537 100644 --- a/ext/js/display/display-notification.js +++ b/ext/js/display/display-notification.js @@ -34,7 +34,7 @@ export class DisplayNotification { this._closeButton = /** @type {HTMLElement} */ (node.querySelector('.footer-notification-close-button')); /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._closeTimer = null; } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index f14291d1..6e1450c3 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -117,7 +117,7 @@ export class Display extends EventDispatcher { this._queryOffset = 0; /** @type {HTMLElement} */ this._progressIndicator = /** @type {HTMLElement} */ (document.querySelector('#progress-indicator')); - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._progressIndicatorTimer = null; /** @type {DynamicProperty} */ this._progressIndicatorVisible = new DynamicProperty(false); diff --git a/ext/js/display/element-overflow-controller.js b/ext/js/display/element-overflow-controller.js index 1d2c808f..35277fa5 100644 --- a/ext/js/display/element-overflow-controller.js +++ b/ext/js/display/element-overflow-controller.js @@ -22,7 +22,7 @@ export class ElementOverflowController { constructor() { /** @type {Element[]} */ this._elements = []; - /** @type {?number} */ + /** @type {?(number|import('core').Timeout)} */ this._checkTimer = null; /** @type {EventListenerCollection} */ this._eventListeners = new EventListenerCollection(); @@ -154,7 +154,7 @@ export class ElementOverflowController { /** * @param {() => void} callback * @param {number} timeout - * @returns {number} + * @returns {number|import('core').Timeout} */ _requestIdleCallback(callback, timeout) { if (typeof requestIdleCallback === 'function') { @@ -165,11 +165,11 @@ export class ElementOverflowController { } /** - * @param {number} handle + * @param {number|import('core').Timeout} handle */ _cancelIdleCallback(handle) { if (typeof cancelIdleCallback === 'function') { - cancelIdleCallback(handle); + cancelIdleCallback(/** @type {number} */ (handle)); } else { clearTimeout(handle); } diff --git a/ext/js/display/option-toggle-hotkey-handler.js b/ext/js/display/option-toggle-hotkey-handler.js index 531e208d..edd7de5b 100644 --- a/ext/js/display/option-toggle-hotkey-handler.js +++ b/ext/js/display/option-toggle-hotkey-handler.js @@ -29,7 +29,7 @@ export class OptionToggleHotkeyHandler { this._display = display; /** @type {?import('./display-notification.js').DisplayNotification} */ this._notification = null; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._notificationHideTimer = null; /** @type {number} */ this._notificationHideTimeout = 5000; diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js index b93757c2..98a4666b 100644 --- a/ext/js/display/search-display-controller.js +++ b/ext/js/display/search-display-controller.js @@ -63,7 +63,7 @@ export class SearchDisplayController { this._wanakanaBound = false; /** @type {boolean} */ this._introVisible = true; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._introAnimationTimer = null; /** @type {boolean} */ this._clipboardMonitorEnabled = false; diff --git a/ext/js/dom/document-util.js b/ext/js/dom/document-util.js index cf58d39f..549a8195 100644 --- a/ext/js/dom/document-util.js +++ b/ext/js/dom/document-util.js @@ -360,7 +360,7 @@ export class DocumentUtil { /** * Adds a fullscreen change event listener. This function handles all of the browser-specific variants. * @param {EventListener} onFullscreenChanged The event callback. - * @param {?EventListenerCollection} eventListenerCollection An optional `EventListenerCollection` to add the registration to. + * @param {?import('../core.js').EventListenerCollection} eventListenerCollection An optional `EventListenerCollection` to add the registration to. */ static addFullscreenChangeEventListener(onFullscreenChanged, eventListenerCollection=null) { const target = document; diff --git a/ext/js/dom/panel-element.js b/ext/js/dom/panel-element.js index 314eb2fd..748c3a36 100644 --- a/ext/js/dom/panel-element.js +++ b/ext/js/dom/panel-element.js @@ -37,7 +37,7 @@ export class PanelElement extends EventDispatcher { this._mutationObserver = null; /** @type {boolean} */ this._visible = false; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._closeTimer = null; } diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js index f6bcde8d..d1b033e6 100644 --- a/ext/js/language/text-scanner.js +++ b/ext/js/language/text-scanner.js @@ -120,7 +120,7 @@ export class TextScanner extends EventDispatcher { /** @type {boolean} */ this._preventNextClickScan = false; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._preventNextClickScanTimer = null; /** @type {number} */ this._preventNextClickScanTimerDuration = 50; @@ -145,7 +145,7 @@ export class TextScanner extends EventDispatcher { /** @type {boolean} */ this._canClearSelection = true; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._textSelectionTimer = null; /** @type {boolean} */ this._yomitanIsChangingTextSelectionNow = false; @@ -995,7 +995,7 @@ export class TextScanner extends EventDispatcher { async _scanTimerWait() { const delay = this._delay; const promise = /** @type {Promise} */ (new Promise((resolve) => { - /** @type {?number} */ + /** @type {?import('core').Timeout} */ let timeout = setTimeout(() => { timeout = null; resolve(true); diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index 0847d479..e041cc67 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -316,7 +316,7 @@ export class AudioDownloader { let signal; /** @type {?import('request-builder.js').ProgressCallback} */ let onProgress = null; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ let idleTimer = null; if (typeof idleTimeout === 'number') { const abortController = new AbortController(); diff --git a/ext/js/pages/settings/popup-preview-frame.js b/ext/js/pages/settings/popup-preview-frame.js index bb00829f..dab21f4d 100644 --- a/ext/js/pages/settings/popup-preview-frame.js +++ b/ext/js/pages/settings/popup-preview-frame.js @@ -43,7 +43,7 @@ export class PopupPreviewFrame { this._apiOptionsGetOld = null; /** @type {boolean} */ this._popupShown = false; - /** @type {?number} */ + /** @type {?import('core').Timeout} */ this._themeChangeTimeout = null; /** @type {?import('text-source').TextSource} */ this._textSource = null; diff --git a/test/jsconfig.json b/test/jsconfig.json index 261ec345..934aab81 100644 --- a/test/jsconfig.json +++ b/test/jsconfig.json @@ -31,7 +31,7 @@ "../ext/**/*.js", "../types/ext/**/*.ts", "../types/dev/**/*.ts", - "../types/other/web-set-timeout.d.ts" + "../types/other/globals.d.ts" ], "exclude": [ "../node_modules", diff --git a/types/ext/core.d.ts b/types/ext/core.d.ts index ce3a09f0..b83e6a74 100644 --- a/types/ext/core.d.ts +++ b/types/ext/core.d.ts @@ -100,3 +100,5 @@ export type MessageHandlerDetails = { export type MessageHandlerMap = Map; export type MessageHandlerArray = [key: string, handlerDetails: MessageHandlerDetails][]; + +export type Timeout = number | NodeJS.Timeout; diff --git a/types/ext/cross-frame-api.d.ts b/types/ext/cross-frame-api.d.ts index 5cedbec9..88ce59a7 100644 --- a/types/ext/cross-frame-api.d.ts +++ b/types/ext/cross-frame-api.d.ts @@ -50,5 +50,5 @@ export type Invocation = { responseTimeout: number; action: string; ack: boolean; - timer: number | null; + timer: Core.Timeout | null; }; diff --git a/types/ext/offscreen.d.ts b/types/ext/offscreen.d.ts index 7dd64d1e..ddf7eadc 100644 --- a/types/ext/offscreen.d.ts +++ b/types/ext/offscreen.d.ts @@ -112,4 +112,4 @@ export type MessageHandler< details: MessageDetailsMap[TMessage], ) => (TIsAsync extends true ? Promise> : MessageReturn); -export type MessageHandlerMap = Map; +export type MessageHandlerMap = Map; diff --git a/types/other/globals.d.ts b/types/other/globals.d.ts new file mode 100644 index 00000000..330f16c2 --- /dev/null +++ b/types/other/globals.d.ts @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 Yomitan 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 . + */ + +declare global { + function clearTimeout(timeoutId: NodeJS.Timeout | string | number | undefined): void; +} + +export {}; diff --git a/types/other/web-set-timeout.d.ts b/types/other/web-set-timeout.d.ts deleted file mode 100644 index 98e40dab..00000000 --- a/types/other/web-set-timeout.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2023 Yomitan 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 . - */ - -declare module 'web-set-timeout' { - global { - // These types are used to ensure that setTimeout returns a number - function setTimeout(callback: (...args: TArgs) => void, ms?: number, ...args: TArgs): number; - function setTimeout(callback: (args: void) => void, ms?: number): number; - } -} -- cgit v1.2.3 From dcddbee07e20163ae167dd67fe58f0776f9acb64 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 4 Dec 2023 18:20:05 -0500 Subject: Update how ts comments are handled --- .eslintrc.json | 9 ++++++++- dev/translator-vm.js | 2 +- ext/js/accessibility/google-docs.js | 6 +++--- ext/js/app/popup.js | 2 +- ext/js/background/offscreen-proxy.js | 8 ++++---- ext/js/comm/frame-ancestry-handler.js | 4 ++-- ext/js/dom/document-util.js | 14 ++++++------- ext/js/dom/dom-text-scanner.js | 5 ++--- ext/js/dom/simple-dom-parser.js | 3 +-- ext/js/pages/settings/backup-controller.js | 4 ++-- ext/js/yomitan.js | 4 ++-- test/cache-map.test.js | 2 +- test/data/html/test-document2-script.js | 30 ++++++++++++++-------------- test/object-property-accessor.test.js | 6 +++--- test/profile-conditions-util.test.js | 32 +++++++++++++++--------------- vitest.config.js | 2 +- 16 files changed, 69 insertions(+), 64 deletions(-) (limited to 'ext/js/comm') diff --git a/.eslintrc.json b/.eslintrc.json index a46aff4b..83e7ffe6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -350,7 +350,14 @@ "allowAllPropertiesOnSameLine": true } ], - "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-expect-error": { + "descriptionFormat": "^ - .+$" + } + } + ], "@typescript-eslint/ban-types": [ "error", { diff --git a/dev/translator-vm.js b/dev/translator-vm.js index 4022d465..7fdda879 100644 --- a/dev/translator-vm.js +++ b/dev/translator-vm.js @@ -42,7 +42,7 @@ export class TranslatorVM { } } }; - // @ts-ignore - Overwriting a global + // @ts-expect-error - Overwriting a global global.chrome = chrome; /** @type {?JapaneseUtil} */ diff --git a/ext/js/accessibility/google-docs.js b/ext/js/accessibility/google-docs.js index da6ab994..27841b6d 100644 --- a/ext/js/accessibility/google-docs.js +++ b/ext/js/accessibility/google-docs.js @@ -18,9 +18,9 @@ (async () => { // Reentrant check - // @ts-ignore : Checking a property to the global object + // @ts-expect-error - Checking a property to the global object if (self.googleDocsAccessibilitySetup) { return; } - // @ts-ignore : Adding a property to the global object + // @ts-expect-error - Adding a property to the global object self.googleDocsAccessibilitySetup = true; /** @@ -57,7 +57,7 @@ // The extension ID below is on an allow-list that is used on the Google Docs webpage. /* eslint-disable */ - // @ts-ignore : Adding a property to the global object + // @ts-expect-error : Adding a property to the global object const inject = () => { window._docs_annotate_canvas_by_ext = 'ogmnaimimemjmbakcfefmnahgdfhfami'; }; /* eslint-enable */ diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index 4f201fc3..7419785b 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -745,7 +745,7 @@ export class Popup extends EventDispatcher { if ( fullscreenElement === null || fullscreenElement.shadowRoot || - // @ts-ignore - openOrClosedShadowRoot is available to Firefox 63+ for WebExtensions + // @ts-expect-error - openOrClosedShadowRoot is available to Firefox 63+ for WebExtensions fullscreenElement.openOrClosedShadowRoot ) { return defaultParent; diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js index 7b504855..63f619fa 100644 --- a/ext/js/background/offscreen-proxy.js +++ b/ext/js/background/offscreen-proxy.js @@ -54,16 +54,16 @@ export class OffscreenProxy { */ async _hasOffscreenDocument() { const offscreenUrl = chrome.runtime.getURL('offscreen.html'); - // @ts-ignore - API not defined yet + // @ts-expect-error - 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 + // @ts-expect-error - Types not set up for service workers yet const matchedClients = await clients.matchAll(); - // @ts-ignore - Types not set up for service workers yet + // @ts-expect-error - Types not set up for service workers yet return await matchedClients.some((client) => client.url === offscreenUrl); } - // @ts-ignore - API not defined yet + // @ts-expect-error - API not defined yet const contexts = await chrome.runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'], documentUrls: [offscreenUrl] diff --git a/ext/js/comm/frame-ancestry-handler.js b/ext/js/comm/frame-ancestry-handler.js index 687ec368..e4d08f28 100644 --- a/ext/js/comm/frame-ancestry-handler.js +++ b/ext/js/comm/frame-ancestry-handler.js @@ -295,7 +295,7 @@ export class FrameAncestryHandler { while (walker.nextNode()) { const element = /** @type {Element} */ (walker.currentNode); - // @ts-ignore - this is more simple to elide any type checks or casting + // @ts-expect-error - this is more simple to elide any type checks or casting if (element.contentWindow === contentWindow) { return element; } @@ -303,7 +303,7 @@ export class FrameAncestryHandler { /** @type {?ShadowRoot|undefined} */ const shadowRoot = ( element.shadowRoot || - // @ts-ignore - openOrClosedShadowRoot is available to Firefox 63+ for WebExtensions + // @ts-expect-error - openOrClosedShadowRoot is available to Firefox 63+ for WebExtensions element.openOrClosedShadowRoot ); if (shadowRoot) { diff --git a/ext/js/dom/document-util.js b/ext/js/dom/document-util.js index 549a8195..b2aa8f81 100644 --- a/ext/js/dom/document-util.js +++ b/ext/js/dom/document-util.js @@ -210,7 +210,7 @@ export class DocumentUtil { */ static computeZoomScale(node) { if (this._cssZoomSupported === null) { - // @ts-ignore - zoom is a non-standard property that exists in Chromium-based browsers + // @ts-expect-error - zoom is a non-standard property that exists in Chromium-based browsers this._cssZoomSupported = (typeof document.createElement('div').style.zoom === 'string'); } if (!this._cssZoomSupported) { return 1; } @@ -387,11 +387,11 @@ export class DocumentUtil { static getFullscreenElement() { return ( document.fullscreenElement || - // @ts-ignore - vendor prefix + // @ts-expect-error - vendor prefix document.msFullscreenElement || - // @ts-ignore - vendor prefix + // @ts-expect-error - vendor prefix document.mozFullScreenElement || - // @ts-ignore - vendor prefix + // @ts-expect-error - vendor prefix document.webkitFullscreenElement || null ); @@ -808,7 +808,7 @@ export class DocumentUtil { return document.caretRangeFromPoint(x, y); } - // @ts-ignore - caretPositionFromPoint is non-standard + // @ts-expect-error - caretPositionFromPoint is non-standard if (typeof document.caretPositionFromPoint === 'function') { // Firefox return this._caretPositionFromPoint(x, y); @@ -824,7 +824,7 @@ export class DocumentUtil { * @returns {?Range} */ static _caretPositionFromPoint(x, y) { - // @ts-ignore - caretPositionFromPoint is non-standard + // @ts-expect-error - caretPositionFromPoint is non-standard const position = /** @type {(x: number, y: number) => ?{offsetNode: Node, offset: number}} */ (document.caretPositionFromPoint)(x, y); if (position === null) { return null; @@ -876,7 +876,7 @@ export class DocumentUtil { nextElement.style.setProperty('user-select', 'text', 'important'); } - // @ts-ignore - caretPositionFromPoint is non-standard + // @ts-expect-error - caretPositionFromPoint is non-standard const position = /** @type {(x: number, y: number) => ?{offsetNode: Node, offset: number}} */ (document.caretPositionFromPoint)(x, y); if (position === null) { return null; diff --git a/ext/js/dom/dom-text-scanner.js b/ext/js/dom/dom-text-scanner.js index 3a785680..42e0acc9 100644 --- a/ext/js/dom/dom-text-scanner.js +++ b/ext/js/dom/dom-text-scanner.js @@ -520,11 +520,10 @@ export class DOMTextScanner { static isStyleSelectable(style) { return !( style.userSelect === 'none' || - // @ts-ignore - vendor prefix style.webkitUserSelect === 'none' || - // @ts-ignore - vendor prefix + // @ts-expect-error - vendor prefix style.MozUserSelect === 'none' || - // @ts-ignore - vendor prefix + // @ts-expect-error - vendor prefix style.msUserSelect === 'none' ); } diff --git a/ext/js/dom/simple-dom-parser.js b/ext/js/dom/simple-dom-parser.js index a1f63890..bca1cd88 100644 --- a/ext/js/dom/simple-dom-parser.js +++ b/ext/js/dom/simple-dom-parser.js @@ -27,7 +27,7 @@ export class SimpleDOMParser { */ constructor(content) { /** @type {import('parse5')} */ - // @ts-ignore - parse5 global is not defined in typescript declaration + // @ts-expect-error - parse5 global is not defined in typescript declaration this._parse5Lib = /** @type {import('parse5')} */ (parse5); /** @type {import('parse5').TreeAdapter} */ this._treeAdapter = this._parse5Lib.defaultTreeAdapter; @@ -131,7 +131,6 @@ export class SimpleDOMParser { * @returns {boolean} */ static isSupported() { - // @ts-ignore - parse5 global is not defined in typescript declaration return typeof parse5 !== 'undefined'; } diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js index 52c5f418..bf44bb90 100644 --- a/ext/js/pages/settings/backup-controller.js +++ b/ext/js/pages/settings/backup-controller.js @@ -165,9 +165,9 @@ export class BackupController { _saveBlob(blob, fileName) { if ( typeof navigator === 'object' && navigator !== null && - // @ts-ignore - call for legacy Edge + // @ts-expect-error - call for legacy Edge typeof navigator.msSaveBlob === 'function' && - // @ts-ignore - call for legacy Edge + // @ts-expect-error - call for legacy Edge navigator.msSaveBlob(blob) ) { return; diff --git a/ext/js/yomitan.js b/ext/js/yomitan.js index 3c0f7cb9..7cf67aec 100644 --- a/ext/js/yomitan.js +++ b/ext/js/yomitan.js @@ -37,7 +37,7 @@ if ((() => { } return (hasBrowser && !hasChrome); })()) { - // @ts-ignore - objects should have roughly the same interface + // @ts-expect-error - objects should have roughly the same interface chrome = browser; } @@ -182,7 +182,7 @@ export class Yomitan extends EventDispatcher { */ sendMessage(...args) { try { - // @ts-ignore - issue with type conversion, somewhat difficult to resolve in pure JS + // @ts-expect-error - issue with type conversion, somewhat difficult to resolve in pure JS chrome.runtime.sendMessage(...args); } catch (e) { this.triggerExtensionUnloaded(); diff --git a/test/cache-map.test.js b/test/cache-map.test.js index 3e7def1f..df891188 100644 --- a/test/cache-map.test.js +++ b/test/cache-map.test.js @@ -30,7 +30,7 @@ function testConstructor() { [true, () => new CacheMap(1.5)], [true, () => new CacheMap(Number.NaN)], [true, () => new CacheMap(Number.POSITIVE_INFINITY)], - // @ts-ignore - Ignore because it should throw an error + // @ts-expect-error - Ignore because it should throw an error [true, () => new CacheMap('a')] ]; diff --git a/test/data/html/test-document2-script.js b/test/data/html/test-document2-script.js index 5a6ad4d1..f6082802 100644 --- a/test/data/html/test-document2-script.js +++ b/test/data/html/test-document2-script.js @@ -22,17 +22,17 @@ function requestFullscreen(element) { if (element.requestFullscreen) { element.requestFullscreen(); - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility } else if (element.mozRequestFullScreen) { - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility element.mozRequestFullScreen(); - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility } else if (element.webkitRequestFullscreen) { - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility element.webkitRequestFullscreen(); - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility } else if (element.msRequestFullscreen) { - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility element.msRequestFullscreen(); } } @@ -41,17 +41,17 @@ function requestFullscreen(element) { function exitFullscreen() { if (document.exitFullscreen) { document.exitFullscreen(); - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility } else if (document.mozCancelFullScreen) { - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility document.mozCancelFullScreen(); - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility } else if (document.webkitExitFullscreen) { - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility document.webkitExitFullscreen(); - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility } else if (document.msExitFullscreen) { - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility document.msExitFullscreen(); } } @@ -62,11 +62,11 @@ function exitFullscreen() { function getFullscreenElement() { return ( document.fullscreenElement || - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility document.msFullscreenElement || - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility document.mozFullScreenElement || - // @ts-ignore - Browser compatibility + // @ts-expect-error - Browser compatibility document.webkitFullscreenElement || null ); diff --git a/test/object-property-accessor.test.js b/test/object-property-accessor.test.js index 4f1fa87b..4d50b1e9 100644 --- a/test/object-property-accessor.test.js +++ b/test/object-property-accessor.test.js @@ -334,7 +334,7 @@ function testGetPathString2() { ]; for (const [pathArray, message] of data) { - // @ts-ignore - Throwing is expected + // @ts-expect-error - Throwing is expected expect(() => ObjectPropertyAccessor.getPathString(pathArray)).toThrow(message); } }); @@ -424,7 +424,7 @@ function testHasProperty() { ]; for (const [object, property, expected] of data) { - // @ts-ignore - Ignore potentially property types + // @ts-expect-error - Ignore potentially property types expect(ObjectPropertyAccessor.hasProperty(object, property)).toStrictEqual(expected); } }); @@ -449,7 +449,7 @@ function testIsValidPropertyType() { ]; for (const [object, property, expected] of data) { - // @ts-ignore - Ignore potentially property types + // @ts-expect-error - Ignore potentially property types expect(ObjectPropertyAccessor.isValidPropertyType(object, property)).toStrictEqual(expected); } }); diff --git a/test/profile-conditions-util.test.js b/test/profile-conditions-util.test.js index 30052b34..62b21555 100644 --- a/test/profile-conditions-util.test.js +++ b/test/profile-conditions-util.test.js @@ -637,9 +637,9 @@ function testSchemas() { {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, @@ -673,9 +673,9 @@ function testSchemas() { {expected: false, context: {depth: 0, url: ''}}, {expected: false, context: {depth: 0, url: '', flags: []}}, {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, @@ -711,9 +711,9 @@ function testSchemas() { {expected: false, context: {depth: 0, url: ''}}, {expected: false, context: {depth: 0, url: '', flags: []}}, {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, @@ -753,9 +753,9 @@ function testSchemas() { {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, @@ -784,9 +784,9 @@ function testSchemas() { {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, @@ -819,9 +819,9 @@ function testSchemas() { {expected: false, context: {depth: 0, url: ''}}, {expected: false, context: {depth: 0, url: '', flags: []}}, {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, @@ -849,9 +849,9 @@ function testSchemas() { {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, @@ -885,9 +885,9 @@ function testSchemas() { {expected: true, context: {depth: 0, url: ''}}, {expected: true, context: {depth: 0, url: '', flags: []}}, {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}}, - // @ts-ignore - Ignore type for string flag for testing purposes + // @ts-expect-error - Ignore type for string flag for testing purposes {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}} ] }, diff --git a/vitest.config.js b/vitest.config.js index 3b6cdde0..025eec17 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -24,7 +24,7 @@ export default defineConfig({ 'test/playwright/**' ], environment: 'jsdom', - // @ts-ignore - Appears to not be defined in the type definitions (https://vitest.dev/advanced/pool) + // @ts-expect-error - Appears to not be defined in the type definitions (https://vitest.dev/advanced/pool) poolOptions: { threads: { useAtomics: true -- cgit v1.2.3