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/language/translator.js | 788 ++++++++++++++++++++++++++++++++---------- 1 file changed, 614 insertions(+), 174 deletions(-) (limited to 'ext/js/language/translator.js') diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 4044f379..9b01c1ff 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -25,35 +25,29 @@ import {DictionaryDatabase} from './dictionary-database.js'; * Class which finds term and kanji dictionary entries for text. */ export class Translator { - /** - * Information about how popup content should be shown, specifically related to the outer popup frame. - * @typedef {object} TermFrequency - * @property {string} term The term. - * @property {string} reading The reading of the term. - * @property {string} dictionary The name of the dictionary that the term frequency originates from. - * @property {boolean} hasReading Whether or not a reading was specified. - * @property {number|string} frequency The frequency value for the term. - */ - /** * Creates a new Translator instance. - * @param {object} details The details for the class. - * @param {JapaneseUtil} details.japaneseUtil An instance of JapaneseUtil. - * @param {DictionaryDatabase} details.database An instance of DictionaryDatabase. + * @param {import('translator').ConstructorDetails} details The details for the class. */ constructor({japaneseUtil, database}) { + /** @type {JapaneseUtil} */ this._japaneseUtil = japaneseUtil; + /** @type {DictionaryDatabase} */ this._database = database; + /** @type {?Deinflector} */ this._deinflector = null; + /** @type {import('translator').DictionaryTagCache} */ this._tagCache = new Map(); + /** @type {Intl.Collator} */ this._stringComparer = new Intl.Collator('en-US'); // Invariant locale + /** @type {RegExp} */ this._numberRegex = /[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?/; } /** * Initializes the instance for use. The public API should not be used until * this function has been called. - * @param {object} deinflectionReasons The raw deinflections reasons data that the Deinflector uses. + * @param {import('deinflector').ReasonsRaw} deinflectionReasons The raw deinflections reasons data that the Deinflector uses. */ prepare(deinflectionReasons) { this._deinflector = new Deinflector(deinflectionReasons); @@ -68,22 +62,23 @@ export class Translator { /** * Finds term definitions for the given text. - * @param {string} mode The mode to use for finding terms, which determines the format of the resulting array. + * @param {import('translator').FindTermsMode} mode The mode to use for finding terms, which determines the format of the resulting array. * One of: 'group', 'merge', 'split', 'simple' * @param {string} text The text to find terms for. - * @param {Translation.FindTermsOptions} options A object describing settings about the lookup. - * @returns {{dictionaryEntries: Translation.TermDictionaryEntry[], originalTextLength: number}} An object containing dictionary entries and the length of the original source text. + * @param {import('translation').FindTermsOptions} options A object describing settings about the lookup. + * @returns {Promise<{dictionaryEntries: import('dictionary').TermDictionaryEntry[], originalTextLength: number}>} An object containing dictionary entries and the length of the original source text. */ async findTerms(mode, text, options) { const {enabledDictionaryMap, excludeDictionaryDefinitions, sortFrequencyDictionary, sortFrequencyDictionaryOrder} = options; - let {dictionaryEntries, originalTextLength} = await this._findTermsInternal(text, enabledDictionaryMap, options); + const tagAggregator = new TranslatorTagAggregator(); + let {dictionaryEntries, originalTextLength} = await this._findTermsInternal(text, enabledDictionaryMap, options, tagAggregator); switch (mode) { case 'group': - dictionaryEntries = this._groupDictionaryEntriesByHeadword(dictionaryEntries); + dictionaryEntries = this._groupDictionaryEntriesByHeadword(dictionaryEntries, tagAggregator); break; case 'merge': - dictionaryEntries = await this._getRelatedDictionaryEntries(dictionaryEntries, options.mainDictionary, enabledDictionaryMap); + dictionaryEntries = await this._getRelatedDictionaryEntries(dictionaryEntries, options.mainDictionary, enabledDictionaryMap, tagAggregator); break; } @@ -91,17 +86,19 @@ export class Translator { this._removeExcludedDefinitions(dictionaryEntries, excludeDictionaryDefinitions); } - if (mode === 'simple') { + if (mode !== 'simple') { + await this._addTermMeta(dictionaryEntries, enabledDictionaryMap, tagAggregator); + await this._expandTagGroupsAndGroup(tagAggregator.getTagExpansionTargets()); + } else { if (sortFrequencyDictionary !== null) { - const sortDictionaryMap = [sortFrequencyDictionary] - .filter((key) => enabledDictionaryMap.has(key)) - .reduce((subMap, key) => subMap.set(key, enabledDictionaryMap.get(key)), new Map()); - await this._addTermMeta(dictionaryEntries, sortDictionaryMap); + /** @type {import('translation').TermEnabledDictionaryMap} */ + const sortDictionaryMap = new Map(); + const value = enabledDictionaryMap.get(sortFrequencyDictionary); + if (typeof value !== 'undefined') { + sortDictionaryMap.set(sortFrequencyDictionary, value); + } + await this._addTermMeta(dictionaryEntries, sortDictionaryMap, tagAggregator); } - this._clearTermTags(dictionaryEntries); - } else { - await this._addTermMeta(dictionaryEntries, enabledDictionaryMap); - await this._expandTermTags(dictionaryEntries); } if (sortFrequencyDictionary !== null) { @@ -125,8 +122,8 @@ export class Translator { * @param {string} text The text to find kanji definitions for. This string can be of any length, * but is typically just one character, which is a single kanji. If the string is multiple * characters long, each character will be searched in the database. - * @param {Translation.FindKanjiOptions} options A object describing settings about the lookup. - * @returns {Translation.KanjiDictionaryEntry[]} An array of definitions. See the _createKanjiDefinition() function for structure details. + * @param {import('translation').FindKanjiOptions} options A object describing settings about the lookup. + * @returns {Promise} An array of definitions. See the _createKanjiDefinition() function for structure details. */ async findKanji(text, options) { const {enabledDictionaryMap} = options; @@ -140,19 +137,18 @@ export class Translator { this._sortDatabaseEntriesByIndex(databaseEntries); + /** @type {import('dictionary').KanjiDictionaryEntry[]} */ const dictionaryEntries = []; + const tagAggregator = new TranslatorTagAggregator(); for (const {character, onyomi, kunyomi, tags, definitions, stats, dictionary} of databaseEntries) { const expandedStats = await this._expandKanjiStats(stats, dictionary); - - const tagGroups = []; - if (tags.length > 0) { tagGroups.push(this._createTagGroup(dictionary, tags)); } - - const dictionaryEntry = this._createKanjiDictionaryEntry(character, dictionary, onyomi, kunyomi, tagGroups, expandedStats, definitions); + const dictionaryEntry = this._createKanjiDictionaryEntry(character, dictionary, onyomi, kunyomi, expandedStats, definitions); dictionaryEntries.push(dictionaryEntry); + tagAggregator.addTags(dictionaryEntry.tags, dictionary, tags); } await this._addKanjiMeta(dictionaryEntries, enabledDictionaryMap); - await this._expandKanjiTags(dictionaryEntries); + await this._expandTagGroupsAndGroup(tagAggregator.getTagExpansionTargets()); this._sortKanjiDictionaryEntryData(dictionaryEntries); @@ -164,8 +160,8 @@ export class Translator { * and a list of dictionaries. * @param {{term: string, reading: string|null}[]} termReadingList An array of `{term, reading}` pairs. If reading is null, * the reading won't be compared. - * @param {Iterable} dictionaries An array of dictionary names. - * @returns {TermFrequency[]} An array of term frequencies. + * @param {string[]} dictionaries An array of dictionary names. + * @returns {Promise} An array of term frequencies. */ async getTermFrequencies(termReadingList, dictionaries) { const dictionarySet = new Set(); @@ -176,25 +172,26 @@ export class Translator { const termList = termReadingList.map(({term}) => term); const metas = await this._database.findTermMetaBulk(termList, dictionarySet); + /** @type {import('translator').TermFrequencySimple[]} */ const results = []; for (const {mode, data, dictionary, index} of metas) { if (mode !== 'freq') { continue; } let {term, reading} = termReadingList[index]; - let frequency = data; - const hasReading = (data !== null && typeof data === 'object'); - if (hasReading) { - if (data.reading !== reading) { - if (reading !== null) { continue; } - reading = data.reading; - } - frequency = data.frequency; + const hasReading = (data !== null && typeof data === 'object' && typeof data.reading === 'string'); + if (hasReading && data.reading !== reading) { + if (reading !== null) { continue; } + reading = data.reading; } + const frequency = hasReading ? data.frequency : /** @type {import('dictionary-data').GenericFrequencyData} */ (data); + const {frequency: frequencyValue, displayValue, displayValueParsed} = this._getFrequencyInfo(frequency); results.push({ term, reading, dictionary, hasReading, - frequency + frequency: frequencyValue, + displayValue, + displayValueParsed }); } return results; @@ -202,7 +199,14 @@ export class Translator { // Find terms internal implementation - async _findTermsInternal(text, enabledDictionaryMap, options) { + /** + * @param {string} text + * @param {Map} enabledDictionaryMap + * @param {import('translation').FindTermsOptions} options + * @param {TranslatorTagAggregator} tagAggregator + * @returns {Promise<{dictionaryEntries: import('dictionary').TermDictionaryEntry[], originalTextLength: number}>} + */ + async _findTermsInternal(text, enabledDictionaryMap, options, tagAggregator) { if (options.removeNonJapaneseCharacters) { text = this._getJapaneseOnlyText(text); } @@ -221,7 +225,7 @@ export class Translator { for (const databaseEntry of databaseEntries) { const {id} = databaseEntry; if (ids.has(id)) { continue; } - const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, true, enabledDictionaryMap); + const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, true, enabledDictionaryMap, tagAggregator); dictionaryEntries.push(dictionaryEntry); ids.add(id); } @@ -230,11 +234,17 @@ export class Translator { return {dictionaryEntries, originalTextLength}; } + /** + * @param {string} text + * @param {Map} enabledDictionaryMap + * @param {import('translation').FindTermsOptions} options + * @returns {Promise} + */ async _findTermsInternal2(text, enabledDictionaryMap, options) { const deinflections = ( options.deinflect ? this._getAllDeinflections(text, options) : - [this._createDeinflection(text, text, text, 0, [], [])] + [this._createDeinflection(text, text, text, 0, [])] ); if (deinflections.length === 0) { return []; } @@ -271,7 +281,13 @@ export class Translator { // Deinflections and text transformations + /** + * @param {string} text + * @param {import('translation').FindTermsOptions} options + * @returns {import('translation-internal').DatabaseDeinflection[]} + */ _getAllDeinflections(text, options) { + /** @type {import('translation-internal').TextDeinflectionOptionsArrays} */ const textOptionVariantArray = [ this._getTextReplacementsVariants(options), this._getTextOptionEntryVariants(options.convertHalfWidthCharacters), @@ -283,9 +299,10 @@ export class Translator { ]; const jp = this._japaneseUtil; + /** @type {import('translation-internal').DatabaseDeinflection[]} */ const deinflections = []; const used = new Set(); - for (const [textReplacements, halfWidth, numeric, alphabetic, katakana, hiragana, [collapseEmphatic, collapseEmphaticFull]] of this._getArrayVariants(textOptionVariantArray)) { + for (const [textReplacements, halfWidth, numeric, alphabetic, katakana, hiragana, [collapseEmphatic, collapseEmphaticFull]] of /** @type {Generator} */ (this._getArrayVariants(textOptionVariantArray))) { let text2 = text; const sourceMap = new TextSourceMap(text2); if (textReplacements !== null) { @@ -315,14 +332,20 @@ export class Translator { if (used.has(source)) { break; } used.add(source); const rawSource = sourceMap.source.substring(0, sourceMap.getSourceLength(i)); - for (const {term, rules, reasons} of this._deinflector.deinflect(source)) { - deinflections.push(this._createDeinflection(rawSource, source, term, rules, reasons, [])); + for (const {term, rules, reasons} of /** @type {Deinflector} */ (this._deinflector).deinflect(source)) { + deinflections.push(this._createDeinflection(rawSource, source, term, rules, reasons)); } } } return deinflections; } + /** + * @param {string} text + * @param {TextSourceMap} sourceMap + * @param {import('translation').FindTermsTextReplacement[]} replacements + * @returns {string} + */ _applyTextReplacements(text, sourceMap, replacements) { for (const {pattern, replacement} of replacements) { text = RegexUtil.applyTextReplacement(text, sourceMap, pattern, replacement); @@ -330,11 +353,15 @@ export class Translator { return text; } + /** + * @param {string} text + * @returns {string} + */ _getJapaneseOnlyText(text) { const jp = this._japaneseUtil; let length = 0; for (const c of text) { - if (!jp.isCodePointJapanese(c.codePointAt(0))) { + if (!jp.isCodePointJapanese(/** @type {number} */ (c.codePointAt(0)))) { return text.substring(0, length); } length += c.length; @@ -342,6 +369,10 @@ export class Translator { return text; } + /** + * @param {import('translation').FindTermsVariantMode} value + * @returns {boolean[]} + */ _getTextOptionEntryVariants(value) { switch (value) { case 'true': return [true]; @@ -350,7 +381,12 @@ export class Translator { } } + /** + * @param {import('translation').FindTermsOptions} options + * @returns {[collapseEmphatic: boolean, collapseEmphaticFull: boolean][]} + */ _getCollapseEmphaticOptions(options) { + /** @type {[collapseEmphatic: boolean, collapseEmphaticFull: boolean][]} */ const collapseEmphaticOptions = [[false, false]]; switch (options.collapseEmphaticSequences) { case 'true': @@ -363,20 +399,43 @@ export class Translator { return collapseEmphaticOptions; } + /** + * @param {import('translation').FindTermsOptions} options + * @returns {(import('translation').FindTermsTextReplacement[] | null)[]} + */ _getTextReplacementsVariants(options) { return options.textReplacements; } - _createDeinflection(originalText, transformedText, deinflectedText, rules, reasons, databaseEntries) { - return {originalText, transformedText, deinflectedText, rules, reasons, databaseEntries}; + /** + * @param {string} originalText + * @param {string} transformedText + * @param {string} deinflectedText + * @param {import('translation-internal').DeinflectionRuleFlags} rules + * @param {string[]} reasons + * @returns {import('translation-internal').DatabaseDeinflection} + */ + _createDeinflection(originalText, transformedText, deinflectedText, rules, reasons) { + return {originalText, transformedText, deinflectedText, rules, reasons, databaseEntries: []}; } // Term dictionary entry grouping - async _getRelatedDictionaryEntries(dictionaryEntries, mainDictionary, enabledDictionaryMap) { + /** + * @param {import('dictionary').TermDictionaryEntry[]} dictionaryEntries + * @param {string} mainDictionary + * @param {import('translation').TermEnabledDictionaryMap} enabledDictionaryMap + * @param {TranslatorTagAggregator} tagAggregator + * @returns {Promise} + */ + async _getRelatedDictionaryEntries(dictionaryEntries, mainDictionary, enabledDictionaryMap, tagAggregator) { + /** @type {import('translator').SequenceQuery[]} */ const sequenceList = []; + /** @type {import('translator').DictionaryEntryGroup[]} */ const groupedDictionaryEntries = []; + /** @type {Map} */ const groupedDictionaryEntriesMap = new Map(); + /** @type {Map} */ const ungroupedDictionaryEntriesMap = new Map(); for (const dictionaryEntry of dictionaryEntries) { const {definitions: [{id, dictionary, sequences: [sequence]}]} = dictionaryEntry; @@ -400,24 +459,31 @@ export class Translator { if (sequenceList.length > 0) { const secondarySearchDictionaryMap = this._getSecondarySearchDictionaryMap(enabledDictionaryMap); - await this._addRelatedDictionaryEntries(groupedDictionaryEntries, ungroupedDictionaryEntriesMap, sequenceList, enabledDictionaryMap); + await this._addRelatedDictionaryEntries(groupedDictionaryEntries, ungroupedDictionaryEntriesMap, sequenceList, enabledDictionaryMap, tagAggregator); for (const group of groupedDictionaryEntries) { this._sortTermDictionaryEntriesById(group.dictionaryEntries); } if (ungroupedDictionaryEntriesMap.size !== 0 || secondarySearchDictionaryMap.size !== 0) { - await this._addSecondaryRelatedDictionaryEntries(groupedDictionaryEntries, ungroupedDictionaryEntriesMap, enabledDictionaryMap, secondarySearchDictionaryMap); + await this._addSecondaryRelatedDictionaryEntries(groupedDictionaryEntries, ungroupedDictionaryEntriesMap, enabledDictionaryMap, secondarySearchDictionaryMap, tagAggregator); } } const newDictionaryEntries = []; for (const group of groupedDictionaryEntries) { - newDictionaryEntries.push(this._createGroupedDictionaryEntry(group.dictionaryEntries, true)); + newDictionaryEntries.push(this._createGroupedDictionaryEntry(group.dictionaryEntries, true, tagAggregator)); } - newDictionaryEntries.push(...this._groupDictionaryEntriesByHeadword(ungroupedDictionaryEntriesMap.values())); + newDictionaryEntries.push(...this._groupDictionaryEntriesByHeadword(ungroupedDictionaryEntriesMap.values(), tagAggregator)); return newDictionaryEntries; } - async _addRelatedDictionaryEntries(groupedDictionaryEntries, ungroupedDictionaryEntriesMap, sequenceList, enabledDictionaryMap) { + /** + * @param {import('translator').DictionaryEntryGroup[]} groupedDictionaryEntries + * @param {Map} ungroupedDictionaryEntriesMap + * @param {import('translator').SequenceQuery[]} sequenceList + * @param {import('translation').TermEnabledDictionaryMap} enabledDictionaryMap + * @param {TranslatorTagAggregator} tagAggregator + */ + async _addRelatedDictionaryEntries(groupedDictionaryEntries, ungroupedDictionaryEntriesMap, sequenceList, enabledDictionaryMap, tagAggregator) { const databaseEntries = await this._database.findTermsBySequenceBulk(sequenceList); for (const databaseEntry of databaseEntries) { const {dictionaryEntries, ids} = groupedDictionaryEntries[databaseEntry.index]; @@ -425,15 +491,23 @@ export class Translator { if (ids.has(id)) { continue; } const {term} = databaseEntry; - const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, term, term, term, [], false, enabledDictionaryMap); + const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, term, term, term, [], false, enabledDictionaryMap, tagAggregator); dictionaryEntries.push(dictionaryEntry); ids.add(id); ungroupedDictionaryEntriesMap.delete(id); } } - async _addSecondaryRelatedDictionaryEntries(groupedDictionaryEntries, ungroupedDictionaryEntriesMap, enabledDictionaryMap, secondarySearchDictionaryMap) { + /** + * @param {import('translator').DictionaryEntryGroup[]} groupedDictionaryEntries + * @param {Map} ungroupedDictionaryEntriesMap + * @param {import('translation').TermEnabledDictionaryMap} enabledDictionaryMap + * @param {import('translation').TermEnabledDictionaryMap} secondarySearchDictionaryMap + * @param {TranslatorTagAggregator} tagAggregator + */ + async _addSecondaryRelatedDictionaryEntries(groupedDictionaryEntries, ungroupedDictionaryEntriesMap, enabledDictionaryMap, secondarySearchDictionaryMap, tagAggregator) { // Prepare grouping info + /** @type {import('dictionary-database').TermExactRequest[]} */ const termList = []; const targetList = []; const targetMap = new Map(); @@ -484,7 +558,7 @@ export class Translator { for (const {ids, dictionaryEntries} of target.groups) { if (ids.has(id)) { continue; } - const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, sourceText, sourceText, sourceText, [], false, enabledDictionaryMap); + const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, sourceText, sourceText, sourceText, [], false, enabledDictionaryMap, tagAggregator); dictionaryEntries.push(dictionaryEntry); ids.add(id); ungroupedDictionaryEntriesMap.delete(id); @@ -492,7 +566,12 @@ export class Translator { } } - _groupDictionaryEntriesByHeadword(dictionaryEntries) { + /** + * @param {Iterable} dictionaryEntries + * @param {TranslatorTagAggregator} tagAggregator + * @returns {import('dictionary').TermDictionaryEntry[]} + */ + _groupDictionaryEntriesByHeadword(dictionaryEntries, tagAggregator) { const groups = new Map(); for (const dictionaryEntry of dictionaryEntries) { const {inflections, headwords: [{term, reading}]} = dictionaryEntry; @@ -507,13 +586,17 @@ export class Translator { const newDictionaryEntries = []; for (const groupDictionaryEntries of groups.values()) { - newDictionaryEntries.push(this._createGroupedDictionaryEntry(groupDictionaryEntries, false)); + newDictionaryEntries.push(this._createGroupedDictionaryEntry(groupDictionaryEntries, false, tagAggregator)); } return newDictionaryEntries; } // Removing data + /** + * @param {import('dictionary').TermDictionaryEntry[]} dictionaryEntries + * @param {Set} excludeDictionaryDefinitions + */ _removeExcludedDefinitions(dictionaryEntries, excludeDictionaryDefinitions) { for (let i = dictionaryEntries.length - 1; i >= 0; --i) { const dictionaryEntry = dictionaryEntries[i]; @@ -534,6 +617,9 @@ export class Translator { } } + /** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + */ _removeUnusedHeadwords(dictionaryEntry) { const {definitions, pronunciations, frequencies, headwords} = dictionaryEntry; const removeHeadwordIndices = new Set(); @@ -548,6 +634,7 @@ export class Translator { if (removeHeadwordIndices.size === 0) { return; } + /** @type {Map} */ const indexRemap = new Map(); let oldIndex = 0; for (let i = 0, ii = headwords.length; i < ii; ++i) { @@ -566,6 +653,10 @@ export class Translator { this._updateArrayItemsHeadwordIndex(frequencies, indexRemap); } + /** + * @param {import('dictionary').TermDefinition[]} definitions + * @param {Map} indexRemap + */ _updateDefinitionHeadwordIndices(definitions, indexRemap) { for (const {headwordIndices} of definitions) { for (let i = headwordIndices.length - 1; i >= 0; --i) { @@ -579,6 +670,10 @@ export class Translator { } } + /** + * @param {import('dictionary').TermPronunciation[]|import('dictionary').TermFrequency[]} array + * @param {Map} indexRemap + */ _updateArrayItemsHeadwordIndex(array, indexRemap) { for (let i = array.length - 1; i >= 0; --i) { const item = array[i]; @@ -592,6 +687,11 @@ export class Translator { } } + /** + * @param {import('dictionary').TermPronunciation[]|import('dictionary').TermFrequency[]|import('dictionary').TermDefinition[]} array + * @param {Set} excludeDictionaryDefinitions + * @returns {boolean} + */ _removeArrayItemsWithDictionary(array, excludeDictionaryDefinitions) { let changed = false; for (let j = array.length - 1; j >= 0; --j) { @@ -603,45 +703,48 @@ export class Translator { return changed; } - _removeTagGroupsWithDictionary(array, excludeDictionaryDefinitions) { - for (const {tags} of array) { - this._removeArrayItemsWithDictionary(tags, excludeDictionaryDefinitions); + /** + * @param {import('dictionary').Tag[]} array + * @param {Set} excludeDictionaryDefinitions + * @returns {boolean} + */ + _removeArrayItemsWithDictionary2(array, excludeDictionaryDefinitions) { + let changed = false; + for (let j = array.length - 1; j >= 0; --j) { + const {dictionaries} = array[j]; + if (this._hasAny(excludeDictionaryDefinitions, dictionaries)) { continue; } + array.splice(j, 1); + changed = true; } + return changed; } - // Tags - - _getTermTagTargets(dictionaryEntries) { - const tagTargets = []; - for (const {headwords, definitions, pronunciations} of dictionaryEntries) { - this._addTagExpansionTargets(tagTargets, headwords); - this._addTagExpansionTargets(tagTargets, definitions); - for (const {pitches} of pronunciations) { - this._addTagExpansionTargets(tagTargets, pitches); - } + /** + * @param {import('dictionary').TermDefinition[]|import('dictionary').TermHeadword[]} array + * @param {Set} excludeDictionaryDefinitions + */ + _removeTagGroupsWithDictionary(array, excludeDictionaryDefinitions) { + for (const {tags} of array) { + this._removeArrayItemsWithDictionary2(tags, excludeDictionaryDefinitions); } - return tagTargets; - } - - _clearTermTags(dictionaryEntries) { - this._getTermTagTargets(dictionaryEntries); } - async _expandTermTags(dictionaryEntries) { - const tagTargets = this._getTermTagTargets(dictionaryEntries); - await this._expandTagGroups(tagTargets); - this._groupTags(tagTargets); - } + // Tags - async _expandKanjiTags(dictionaryEntries) { - const tagTargets = []; - this._addTagExpansionTargets(tagTargets, dictionaryEntries); - await this._expandTagGroups(tagTargets); - this._groupTags(tagTargets); + /** + * @param {import('translator').TagExpansionTarget[]} tagExpansionTargets + */ + async _expandTagGroupsAndGroup(tagExpansionTargets) { + await this._expandTagGroups(tagExpansionTargets); + this._groupTags(tagExpansionTargets); } + /** + * @param {import('translator').TagExpansionTarget[]} tagTargets + */ async _expandTagGroups(tagTargets) { const allItems = []; + /** @type {import('translator').TagTargetMap} */ const targetMap = new Map(); for (const {tagGroups, tags} of tagTargets) { for (const {dictionary, tagNames} of tagGroups) { @@ -687,10 +790,12 @@ export class Translator { const databaseTags = await this._database.findTagMetaBulk(nonCachedItems); for (let i = 0; i < nonCachedItemCount; ++i) { const item = nonCachedItems[i]; - let databaseTag = databaseTags[i]; - if (typeof databaseTag === 'undefined') { databaseTag = null; } - item.databaseTag = databaseTag; - item.cache.set(item.query, databaseTag); + const databaseTag = databaseTags[i]; + const databaseTag2 = typeof databaseTag !== 'undefined' ? databaseTag : null; + item.databaseTag = databaseTag2; + if (item.cache !== null) { + item.cache.set(item.query, databaseTag2); + } } } @@ -701,8 +806,16 @@ export class Translator { } } + /** + * @param {import('translator').TagExpansionTarget[]} tagTargets + */ _groupTags(tagTargets) { const stringComparer = this._stringComparer; + /** + * @param {import('dictionary').Tag} v1 + * @param {import('dictionary').Tag} v2 + * @returns {number} + */ const compare = (v1, v2) => { const i = v1.order - v2.order; return i !== 0 ? i : stringComparer.compare(v1.name, v2.name); @@ -715,16 +828,9 @@ export class Translator { } } - _addTagExpansionTargets(tagTargets, objects) { - for (const value of objects) { - const tagGroups = value.tags; - if (tagGroups.length === 0) { continue; } - const tags = []; - value.tags = tags; - tagTargets.push({tagGroups, tags}); - } - } - + /** + * @param {import('dictionary').Tag[]} tags + */ _mergeSimilarTags(tags) { let tagCount = tags.length; for (let i = 0; i < tagCount; ++i) { @@ -745,6 +851,11 @@ export class Translator { } } + /** + * @param {import('dictionary').Tag[]} tags + * @param {string} category + * @returns {string[]} + */ _getTagNamesWithCategory(tags, category) { const results = []; for (const tag of tags) { @@ -755,6 +866,9 @@ export class Translator { return results; } + /** + * @param {import('dictionary').TermDefinition[]} definitions + */ _flagRedundantDefinitionTags(definitions) { if (definitions.length === 0) { return; } @@ -789,7 +903,12 @@ export class Translator { // Metadata - async _addTermMeta(dictionaryEntries, enabledDictionaryMap) { + /** + * @param {import('dictionary').TermDictionaryEntry[]} dictionaryEntries + * @param {import('translation').TermEnabledDictionaryMap} enabledDictionaryMap + * @param {TranslatorTagAggregator} tagAggregator + */ + async _addTermMeta(dictionaryEntries, enabledDictionaryMap, tagAggregator) { const headwordMap = new Map(); const headwordMapKeys = []; const headwordReadingMaps = []; @@ -821,16 +940,11 @@ export class Translator { switch (mode) { case 'freq': { - let frequency = data; const hasReading = (data !== null && typeof data === 'object' && typeof data.reading === 'string'); - if (hasReading) { - if (data.reading !== reading) { continue; } - frequency = data.frequency; - } + if (hasReading && data.reading !== reading) { continue; } + const frequency = hasReading ? data.frequency : /** @type {import('dictionary-data').GenericFrequencyData} */ (data); for (const {frequencies, headwordIndex} of targets) { - let displayValue; - let displayValueParsed; - ({frequency, displayValue, displayValueParsed} = this._getFrequencyInfo(frequency)); + const {frequency: frequencyValue, displayValue, displayValueParsed} = this._getFrequencyInfo(frequency); frequencies.push(this._createTermFrequency( frequencies.length, headwordIndex, @@ -838,7 +952,7 @@ export class Translator { dictionaryIndex, dictionaryPriority, hasReading, - frequency, + frequencyValue, displayValue, displayValueParsed )); @@ -848,11 +962,13 @@ export class Translator { case 'pitch': { if (data.reading !== reading) { continue; } + /** @type {import('dictionary').TermPitch[]} */ const pitches = []; for (const {position, tags, nasal, devoice} of data.pitches) { + /** @type {import('dictionary').Tag[]} */ const tags2 = []; - if (Array.isArray(tags) && tags.length > 0) { - tags2.push(this._createTagGroup(dictionary, tags)); + if (Array.isArray(tags)) { + tagAggregator.addTags(tags2, dictionary, tags); } const nasalPositions = this._toNumberArray(nasal); const devoicePositions = this._toNumberArray(devoice); @@ -875,6 +991,10 @@ export class Translator { } } + /** + * @param {import('dictionary').KanjiDictionaryEntry[]} dictionaryEntries + * @param {import('translation').KanjiEnabledDictionaryMap} enabledDictionaryMap + */ async _addKanjiMeta(dictionaryEntries, enabledDictionaryMap) { const kanjiList = []; for (const {character} of dictionaryEntries) { @@ -905,6 +1025,11 @@ export class Translator { } } + /** + * @param {{[key: string]: (string|number)}} stats + * @param {string} dictionary + * @returns {Promise} + */ async _expandKanjiStats(stats, dictionary) { const statsEntries = Object.entries(stats); const items = []; @@ -915,10 +1040,11 @@ export class Translator { const databaseInfos = await this._database.findTagMetaBulk(items); + /** @type {Map} */ const statsGroups = new Map(); for (let i = 0, ii = statsEntries.length; i < ii; ++i) { const databaseInfo = databaseInfos[i]; - if (databaseInfo === null) { continue; } + if (typeof databaseInfo === 'undefined') { continue; } const [name, value] = statsEntries[i]; const {category} = databaseInfo; @@ -931,6 +1057,7 @@ export class Translator { group.push(this._createKanjiStat(name, value, databaseInfo, dictionary)); } + /** @type {import('dictionary').KanjiStatGroups} */ const groupedStats = {}; for (const [category, group] of statsGroups.entries()) { this._sortKanjiStats(group); @@ -939,6 +1066,9 @@ export class Translator { return groupedStats; } + /** + * @param {import('dictionary').KanjiStat[]} stats + */ _sortKanjiStats(stats) { if (stats.length <= 1) { return; } const stringComparer = this._stringComparer; @@ -948,45 +1078,59 @@ export class Translator { }); } + /** + * @param {string} value + * @returns {number} + */ _convertStringToNumber(value) { const match = this._numberRegex.exec(value); if (match === null) { return 0; } - value = Number.parseFloat(match[0]); - return Number.isFinite(value) ? value : 0; + const result = Number.parseFloat(match[0]); + return Number.isFinite(result) ? result : 0; } + /** + * @param {import('dictionary-data').GenericFrequencyData} frequency + * @returns {{frequency: number, displayValue: ?string, displayValueParsed: boolean}} + */ _getFrequencyInfo(frequency) { + let frequencyValue = 0; let displayValue = null; let displayValueParsed = false; if (typeof frequency === 'object' && frequency !== null) { - ({value: frequency, displayValue} = frequency); - if (typeof frequency !== 'number') { frequency = 0; } - if (typeof displayValue !== 'string') { displayValue = null; } + const {value: frequencyValue2, displayValue: displayValue2} = frequency; + if (typeof frequencyValue2 === 'number') { frequencyValue = frequencyValue2; } + if (typeof displayValue2 === 'string') { displayValue = displayValue2; } } else { switch (typeof frequency) { case 'number': - // No change + frequencyValue = frequency; break; case 'string': displayValue = frequency; displayValueParsed = true; - frequency = this._convertStringToNumber(frequency); - break; - default: - frequency = 0; + frequencyValue = this._convertStringToNumber(frequency); break; } } - return {frequency, displayValue, displayValueParsed}; + return {frequency: frequencyValue, displayValue, displayValueParsed}; } // Helpers + /** + * @param {string} name + * @returns {string} + */ _getNameBase(name) { const pos = name.indexOf(':'); return (pos >= 0 ? name.substring(0, pos) : name); } + /** + * @param {import('translation').TermEnabledDictionaryMap} enabledDictionaryMap + * @returns {import('translation').TermEnabledDictionaryMap} + */ _getSecondarySearchDictionaryMap(enabledDictionaryMap) { const secondarySearchDictionaryMap = new Map(); for (const [dictionary, details] of enabledDictionaryMap.entries()) { @@ -996,12 +1140,22 @@ export class Translator { return secondarySearchDictionaryMap; } + /** + * @param {string} dictionary + * @param {import('translation').TermEnabledDictionaryMap|import('translation').KanjiEnabledDictionaryMap} enabledDictionaryMap + * @returns {{index: number, priority: number}} + */ _getDictionaryOrder(dictionary, enabledDictionaryMap) { const info = enabledDictionaryMap.get(dictionary); const {index, priority} = typeof info !== 'undefined' ? info : {index: enabledDictionaryMap.size, priority: 0}; return {index, priority}; } + /** + * @param {[...args: unknown[][]]} arrayVariants + * @yields {[...args: unknown[]]} + * @returns {Generator} + */ *_getArrayVariants(arrayVariants) { const ii = arrayVariants.length; @@ -1022,16 +1176,31 @@ export class Translator { } } + /** + * @param {unknown[]} array + * @returns {string} + */ _createMapKey(array) { return JSON.stringify(array); } + /** + * @param {number|number[]|undefined} value + * @returns {number[]} + */ _toNumberArray(value) { return Array.isArray(value) ? value : (typeof value === 'number' ? [value] : []); } // Kanji data + /** + * @param {string} name + * @param {string|number} value + * @param {import('dictionary-database').Tag} databaseInfo + * @param {string} dictionary + * @returns {import('dictionary').KanjiStat} + */ _createKanjiStat(name, value, databaseInfo, dictionary) { const {category, notes, order, score} = databaseInfo; return { @@ -1040,23 +1209,43 @@ export class Translator { content: (typeof notes === 'string' ? notes : ''), order: (typeof order === 'number' ? order : 0), score: (typeof score === 'number' ? score : 0), - dictionary: (typeof dictionary === 'string' ? dictionary : null), + dictionary, value }; } + /** + * @param {number} index + * @param {string} dictionary + * @param {number} dictionaryIndex + * @param {number} dictionaryPriority + * @param {string} character + * @param {number} frequency + * @param {?string} displayValue + * @param {boolean} displayValueParsed + * @returns {import('dictionary').KanjiFrequency} + */ _createKanjiFrequency(index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue, displayValueParsed) { return {index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue, displayValueParsed}; } - _createKanjiDictionaryEntry(character, dictionary, onyomi, kunyomi, tags, stats, definitions) { + /** + * @param {string} character + * @param {string} dictionary + * @param {string[]} onyomi + * @param {string[]} kunyomi + * @param {import('dictionary').KanjiStatGroups} stats + * @param {string[]} definitions + * @returns {import('dictionary').KanjiDictionaryEntry} + */ + _createKanjiDictionaryEntry(character, dictionary, onyomi, kunyomi, stats, definitions) { return { type: 'kanji', character, dictionary, onyomi, kunyomi, - tags, + tags: [], stats, definitions, frequencies: [] @@ -1065,8 +1254,17 @@ export class Translator { // Term data + /** + * @param {?import('dictionary-database').Tag} databaseTag + * @param {string} name + * @param {string} dictionary + * @returns {import('dictionary').Tag} + */ _createTag(databaseTag, name, dictionary) { - const {category, notes, order, score} = (databaseTag !== null ? databaseTag : {}); + let category, notes, order, score; + if (typeof databaseTag === 'object' && databaseTag !== null) { + ({category, notes, order, score} = databaseTag); + } return { name, category: (typeof category === 'string' && category.length > 0 ? category : 'default'), @@ -1078,18 +1276,46 @@ export class Translator { }; } - _createTagGroup(dictionary, tagNames) { - return {dictionary, tagNames}; - } - + /** + * @param {string} originalText + * @param {string} transformedText + * @param {string} deinflectedText + * @param {import('dictionary').TermSourceMatchType} matchType + * @param {import('dictionary').TermSourceMatchSource} matchSource + * @param {boolean} isPrimary + * @returns {import('dictionary').TermSource} + */ _createSource(originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary) { return {originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary}; } + /** + * @param {number} index + * @param {string} term + * @param {string} reading + * @param {import('dictionary').TermSource[]} sources + * @param {import('dictionary').Tag[]} tags + * @param {string[]} wordClasses + * @returns {import('dictionary').TermHeadword} + */ _createTermHeadword(index, term, reading, sources, tags, wordClasses) { return {index, term, reading, sources, tags, wordClasses}; } + /** + * @param {number} index + * @param {number[]} headwordIndices + * @param {string} dictionary + * @param {number} dictionaryIndex + * @param {number} dictionaryPriority + * @param {number} id + * @param {number} score + * @param {number[]} sequences + * @param {boolean} isPrimary + * @param {import('dictionary').Tag[]} tags + * @param {import('dictionary-data').TermGlossary[]} entries + * @returns {import('dictionary').TermDefinition} + */ _createTermDefinition(index, headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, id, score, sequences, isPrimary, tags, entries) { return { index, @@ -1107,14 +1333,47 @@ export class Translator { }; } + /** + * @param {number} index + * @param {number} headwordIndex + * @param {string} dictionary + * @param {number} dictionaryIndex + * @param {number} dictionaryPriority + * @param {import('dictionary').TermPitch[]} pitches + * @returns {import('dictionary').TermPronunciation} + */ _createTermPronunciation(index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches) { return {index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches}; } + /** + * @param {number} index + * @param {number} headwordIndex + * @param {string} dictionary + * @param {number} dictionaryIndex + * @param {number} dictionaryPriority + * @param {boolean} hasReading + * @param {number} frequency + * @param {?string} displayValue + * @param {boolean} displayValueParsed + * @returns {import('dictionary').TermFrequency} + */ _createTermFrequency(index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue, displayValueParsed) { return {index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue, displayValueParsed}; } + /** + * @param {boolean} isPrimary + * @param {string[]} inflections + * @param {number} score + * @param {number} dictionaryIndex + * @param {number} dictionaryPriority + * @param {number} sourceTermExactMatchCount + * @param {number} maxTransformedTextLength + * @param {import('dictionary').TermHeadword[]} headwords + * @param {import('dictionary').TermDefinition[]} definitions + * @returns {import('dictionary').TermDictionaryEntry} + */ _createTermDictionaryEntry(isPrimary, inflections, score, dictionaryIndex, dictionaryPriority, sourceTermExactMatchCount, maxTransformedTextLength, headwords, definitions) { return { type: 'term', @@ -1133,7 +1392,18 @@ export class Translator { }; } - _createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, isPrimary, enabledDictionaryMap) { + /** + * @param {import('dictionary-database').TermEntry} databaseEntry + * @param {string} originalText + * @param {string} transformedText + * @param {string} deinflectedText + * @param {string[]} reasons + * @param {boolean} isPrimary + * @param {Map} enabledDictionaryMap + * @param {TranslatorTagAggregator} tagAggregator + * @returns {import('dictionary').TermDictionaryEntry} + */ + _createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, isPrimary, enabledDictionaryMap, tagAggregator) { const {matchType, matchSource, term, reading: rawReading, definitionTags, termTags, definitions, score, dictionary, id, sequence: rawSequence, rules} = databaseEntry; const reading = (rawReading.length > 0 ? rawReading : term); const {index: dictionaryIndex, priority: dictionaryPriority} = this._getDictionaryOrder(dictionary, enabledDictionaryMap); @@ -1143,10 +1413,12 @@ export class Translator { const hasSequence = (rawSequence >= 0); const sequence = hasSequence ? rawSequence : -1; + /** @type {import('dictionary').Tag[]} */ const headwordTagGroups = []; + /** @type {import('dictionary').Tag[]} */ const definitionTagGroups = []; - if (termTags.length > 0) { headwordTagGroups.push(this._createTagGroup(dictionary, termTags)); } - if (definitionTags.length > 0) { definitionTagGroups.push(this._createTagGroup(dictionary, definitionTags)); } + tagAggregator.addTags(headwordTagGroups, dictionary, termTags); + tagAggregator.addTags(definitionTagGroups, dictionary, definitionTags); return this._createTermDictionaryEntry( isPrimary, @@ -1161,12 +1433,19 @@ export class Translator { ); } - _createGroupedDictionaryEntry(dictionaryEntries, checkDuplicateDefinitions) { + /** + * @param {import('dictionary').TermDictionaryEntry[]} dictionaryEntries + * @param {boolean} checkDuplicateDefinitions + * @param {TranslatorTagAggregator} tagAggregator + * @returns {import('dictionary').TermDictionaryEntry} + */ + _createGroupedDictionaryEntry(dictionaryEntries, checkDuplicateDefinitions, tagAggregator) { // Headwords are generated before sorting, so that the order of dictionaryEntries can be maintained const definitionEntries = []; + /** @type {Map} */ const headwords = new Map(); for (const dictionaryEntry of dictionaryEntries) { - const headwordIndexMap = this._addTermHeadwords(headwords, dictionaryEntry.headwords); + const headwordIndexMap = this._addTermHeadwords(headwords, dictionaryEntry.headwords, tagAggregator); definitionEntries.push({index: definitionEntries.length, dictionaryEntry, headwordIndexMap}); } @@ -1181,7 +1460,9 @@ export class Translator { let dictionaryPriority = Number.MIN_SAFE_INTEGER; let maxTransformedTextLength = 0; let isPrimary = false; + /** @type {import('dictionary').TermDefinition[]} */ const definitions = []; + /** @type {?Map} */ const definitionsMap = checkDuplicateDefinitions ? new Map() : null; let inflections = null; @@ -1197,8 +1478,8 @@ export class Translator { inflections = dictionaryEntryInflections; } } - if (checkDuplicateDefinitions) { - this._addTermDefinitions(definitions, definitionsMap, dictionaryEntry.definitions, headwordIndexMap); + if (definitionsMap !== null) { + this._addTermDefinitions(definitions, definitionsMap, dictionaryEntry.definitions, headwordIndexMap, tagAggregator); } else { this._addTermDefinitionsFast(definitions, dictionaryEntry.definitions, headwordIndexMap); } @@ -1231,6 +1512,11 @@ export class Translator { // Data collection addition functions + /** + * @template [T=unknown] + * @param {T[]} list + * @param {T[]} newItems + */ _addUniqueSimple(list, newItems) { for (const item of newItems) { if (!list.includes(item)) { @@ -1239,6 +1525,10 @@ export class Translator { } } + /** + * @param {import('dictionary').TermSource[]} sources + * @param {import('dictionary').TermSource[]} newSources + */ _addUniqueSources(sources, newSources) { if (newSources.length === 0) { return; } if (sources.length === 0) { @@ -1267,27 +1557,14 @@ export class Translator { } } - _addUniqueTagGroups(tagGroups, newTagGroups) { - if (newTagGroups.length === 0) { return; } - for (const newTagGroup of newTagGroups) { - const {dictionary} = newTagGroup; - const ii = tagGroups.length; - if (ii > 0) { - let i = 0; - for (; i < ii; ++i) { - const tagGroup = tagGroups[i]; - if (tagGroup.dictionary === dictionary) { - this._addUniqueSimple(tagGroup.tagNames, newTagGroup.tagNames); - break; - } - } - if (i < ii) { continue; } - } - tagGroups.push(newTagGroup); - } - } - - _addTermHeadwords(headwordsMap, headwords) { + /** + * @param {Map} headwordsMap + * @param {import('dictionary').TermHeadword[]} headwords + * @param {TranslatorTagAggregator} tagAggregator + * @returns {number[]} + */ + _addTermHeadwords(headwordsMap, headwords, tagAggregator) { + /** @type {number[]} */ const headwordIndexMap = []; for (const {term, reading, sources, tags, wordClasses} of headwords) { const key = this._createMapKey([term, reading]); @@ -1297,13 +1574,17 @@ export class Translator { headwordsMap.set(key, headword); } this._addUniqueSources(headword.sources, sources); - this._addUniqueTagGroups(headword.tags, tags); this._addUniqueSimple(headword.wordClasses, wordClasses); + tagAggregator.mergeTags(headword.tags, tags); headwordIndexMap.push(headword.index); } return headwordIndexMap; } + /** + * @param {number[]} headwordIndices + * @param {number} headwordIndex + */ _addUniqueTermHeadwordIndex(headwordIndices, headwordIndex) { let end = headwordIndices.length; if (end === 0) { @@ -1327,6 +1608,11 @@ export class Translator { headwordIndices.splice(start, 0, headwordIndex); } + /** + * @param {import('dictionary').TermDefinition[]} definitions + * @param {import('dictionary').TermDefinition[]} newDefinitions + * @param {number[]} headwordIndexMap + */ _addTermDefinitionsFast(definitions, newDefinitions, headwordIndexMap) { for (const {headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, sequences, id, score, isPrimary, tags, entries} of newDefinitions) { const headwordIndicesNew = []; @@ -1337,7 +1623,14 @@ export class Translator { } } - _addTermDefinitions(definitions, definitionsMap, newDefinitions, headwordIndexMap) { + /** + * @param {import('dictionary').TermDefinition[]} definitions + * @param {Map} definitionsMap + * @param {import('dictionary').TermDefinition[]} newDefinitions + * @param {number[]} headwordIndexMap + * @param {TranslatorTagAggregator} tagAggregator + */ + _addTermDefinitions(definitions, definitionsMap, newDefinitions, headwordIndexMap, tagAggregator) { for (const {headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, sequences, id, score, isPrimary, tags, entries} of newDefinitions) { const key = this._createMapKey([dictionary, ...entries]); let definition = definitionsMap.get(key); @@ -1356,19 +1649,36 @@ export class Translator { for (const headwordIndex of headwordIndices) { this._addUniqueTermHeadwordIndex(newHeadwordIndices, headwordIndexMap[headwordIndex]); } - this._addUniqueTagGroups(definition.tags, tags); + tagAggregator.mergeTags(definition.tags, tags); } } // Sorting functions + /** + * @param {import('dictionary-database').TermEntry[]|import('dictionary-database').KanjiEntry[]} databaseEntries + */ _sortDatabaseEntriesByIndex(databaseEntries) { if (databaseEntries.length <= 1) { return; } - databaseEntries.sort((a, b) => a.index - b.index); + /** + * @param {import('dictionary-database').TermEntry|import('dictionary-database').KanjiEntry} v1 + * @param {import('dictionary-database').TermEntry|import('dictionary-database').KanjiEntry} v2 + * @returns {number} + */ + const compareFunction = (v1, v2) => v1.index - v2.index; + databaseEntries.sort(compareFunction); } + /** + * @param {import('dictionary').TermDictionaryEntry[]} dictionaryEntries + */ _sortTermDictionaryEntries(dictionaryEntries) { const stringComparer = this._stringComparer; + /** + * @param {import('dictionary').TermDictionaryEntry} v1 + * @param {import('dictionary').TermDictionaryEntry} v2 + * @returns {number} + */ const compareFunction = (v1, v2) => { // Sort by length of source term let i = v2.maxTransformedTextLength - v1.maxTransformedTextLength; @@ -1419,7 +1729,15 @@ export class Translator { dictionaryEntries.sort(compareFunction); } + /** + * @param {import('dictionary').TermDefinition[]} definitions + */ _sortTermDictionaryEntryDefinitions(definitions) { + /** + * @param {import('dictionary').TermDefinition} v1 + * @param {import('dictionary').TermDefinition} v2 + * @returns {number} + */ const compareFunction = (v1, v2) => { // Sort by frequency order let i = v1.frequencyOrder - v2.frequencyOrder; @@ -1455,12 +1773,23 @@ export class Translator { definitions.sort(compareFunction); } + /** + * @param {import('dictionary').TermDictionaryEntry[]} dictionaryEntries + */ _sortTermDictionaryEntriesById(dictionaryEntries) { if (dictionaryEntries.length <= 1) { return; } dictionaryEntries.sort((a, b) => a.definitions[0].id - b.definitions[0].id); } + /** + * @param {import('dictionary').TermFrequency[]|import('dictionary').TermPronunciation[]} dataList + */ _sortTermDictionaryEntrySimpleData(dataList) { + /** + * @param {import('dictionary').TermFrequency|import('dictionary').TermPronunciation} v1 + * @param {import('dictionary').TermFrequency|import('dictionary').TermPronunciation} v2 + * @returns {number} + */ const compare = (v1, v2) => { // Sort by dictionary priority let i = v2.dictionaryPriority - v1.dictionaryPriority; @@ -1481,7 +1810,15 @@ export class Translator { dataList.sort(compare); } + /** + * @param {import('dictionary').KanjiDictionaryEntry[]} dictionaryEntries + */ _sortKanjiDictionaryEntryData(dictionaryEntries) { + /** + * @param {import('dictionary').KanjiFrequency} v1 + * @param {import('dictionary').KanjiFrequency} v2 + * @returns {number} + */ const compare = (v1, v2) => { // Sort by dictionary priority let i = v2.dictionaryPriority - v1.dictionaryPriority; @@ -1501,6 +1838,11 @@ export class Translator { } } + /** + * @param {import('dictionary').TermDictionaryEntry[]} dictionaryEntries + * @param {string} dictionary + * @param {boolean} ascending + */ _updateSortFrequencies(dictionaryEntries, dictionary, ascending) { const frequencyMap = new Map(); for (const dictionaryEntry of dictionaryEntries) { @@ -1539,4 +1881,102 @@ export class Translator { frequencyMap.clear(); } } + + // Miscellaneous + + /** + * @template T + * @param {Set} set + * @param {T[]} values + * @returns {boolean} + */ + _hasAny(set, values) { + for (const value of values) { + if (set.has(value)) { return true; } + } + return false; + } +} + +class TranslatorTagAggregator { + constructor() { + /** @type {Map} */ + this._tagExpansionTargetMap = new Map(); + } + + /** + * @param {import('dictionary').Tag[]} tags + * @param {string} dictionary + * @param {string[]} tagNames + */ + addTags(tags, dictionary, tagNames) { + if (tagNames.length === 0) { return; } + const tagGroups = this._getOrCreateTagGroups(tags); + const tagGroup = this._getOrCreateTagGroup(tagGroups, dictionary); + this._addUniqueTags(tagGroup, tagNames); + } + + /** + * @returns {import('translator').TagExpansionTarget[]} + */ + getTagExpansionTargets() { + const results = []; + for (const [tags, tagGroups] of this._tagExpansionTargetMap) { + results.push({tags, tagGroups}); + } + return results; + } + + /** + * @param {import('dictionary').Tag[]} tags + * @param {import('dictionary').Tag[]} newTags + */ + mergeTags(tags, newTags) { + const newTagGroups = this._tagExpansionTargetMap.get(newTags); + if (typeof newTagGroups === 'undefined') { return; } + const tagGroups = this._getOrCreateTagGroups(tags); + for (const {dictionary, tagNames} of newTagGroups) { + const tagGroup = this._getOrCreateTagGroup(tagGroups, dictionary); + this._addUniqueTags(tagGroup, tagNames); + } + } + + /** + * @param {import('dictionary').Tag[]} tags + * @returns {import('translator').TagGroup[]} + */ + _getOrCreateTagGroups(tags) { + let tagGroups = this._tagExpansionTargetMap.get(tags); + if (typeof tagGroups === 'undefined') { + tagGroups = []; + this._tagExpansionTargetMap.set(tags, tagGroups); + } + return tagGroups; + } + + /** + * @param {import('translator').TagGroup[]} tagGroups + * @param {string} dictionary + * @returns {import('translator').TagGroup} + */ + _getOrCreateTagGroup(tagGroups, dictionary) { + for (const tagGroup of tagGroups) { + if (tagGroup.dictionary === dictionary) { return tagGroup; } + } + const newTagGroup = {dictionary, tagNames: []}; + tagGroups.push(newTagGroup); + return newTagGroup; + } + + /** + * @param {import('translator').TagGroup} tagGroup + * @param {string[]} newTagNames + */ + _addUniqueTags(tagGroup, newTagNames) { + const {tagNames} = tagGroup; + for (const tagName of newTagNames) { + if (tagNames.includes(tagName)) { continue; } + tagNames.push(tagName); + } + } } -- 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/language/translator.js') 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 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/language/translator.js') 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 29317da4ea237557e1805a834913dad73c51ed8a Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Mon, 27 Nov 2023 17:43:53 -0500 Subject: Update offscreen --- ext/js/background/offscreen-proxy.js | 137 +++++++++++++++++++++++++---------- ext/js/background/offscreen.js | 121 +++++++++++++++++-------------- ext/js/language/translator.js | 4 +- types/ext/dictionary-database.d.ts | 2 +- types/ext/offscreen.d.ts | 115 +++++++++++++++++++++++++++++ types/ext/translator.d.ts | 12 +++ 6 files changed, 298 insertions(+), 93 deletions(-) create mode 100644 types/ext/offscreen.d.ts (limited to 'ext/js/language/translator.js') diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js index 0fb2f269..757d78d5 100644 --- a/ext/js/background/offscreen-proxy.js +++ b/ext/js/background/offscreen-proxy.js @@ -18,13 +18,17 @@ import {isObject} from '../core.js'; import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; +import {ExtensionError} from '../core/extension-error.js'; export class OffscreenProxy { constructor() { + /** @type {?Promise} */ this._creatingOffscreen = null; } - // https://developer.chrome.com/docs/extensions/reference/offscreen/ + /** + * @see https://developer.chrome.com/docs/extensions/reference/offscreen/ + */ async prepare() { if (await this._hasOffscreenDocument()) { return; @@ -36,20 +40,30 @@ export class OffscreenProxy { this._creatingOffscreen = chrome.offscreen.createDocument({ url: 'offscreen.html', - reasons: ['CLIPBOARD'], + reasons: [ + /** @type {chrome.offscreen.Reason} */ ('CLIPBOARD') + ], justification: 'Access to the clipboard' }); await this._creatingOffscreen; this._creatingOffscreen = null; } + /** + * @returns {Promise} + */ async _hasOffscreenDocument() { const offscreenUrl = chrome.runtime.getURL('offscreen.html'); - if (!chrome.runtime.getContexts) { // chrome version <116 + // @ts-ignore - API not defined yet + if (!chrome.runtime.getContexts) { // chrome version below 116 + // Clients: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/clients + // @ts-ignore - Types not set up for service workers yet const matchedClients = await clients.matchAll(); + // @ts-ignore - Types not set up for service workers yet return await matchedClients.some((client) => client.url === offscreenUrl); } + // @ts-ignore - API not defined yet const contexts = await chrome.runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'], documentUrls: [offscreenUrl] @@ -57,103 +71,152 @@ export class OffscreenProxy { return !!contexts.length; } - sendMessagePromise(...args) { + /** + * @template {import('offscreen').MessageType} TMessageType + * @param {import('offscreen').Message} message + * @returns {Promise>} + */ + sendMessagePromise(message) { return new Promise((resolve, reject) => { - const callback = (response) => { + chrome.runtime.sendMessage(message, (response) => { try { resolve(this._getMessageResponseResult(response)); } catch (error) { reject(error); } - }; - - chrome.runtime.sendMessage(...args, callback); + }); }); } + /** + * @template [TReturn=unknown] + * @param {import('core').Response} response + * @returns {TReturn} + * @throws {Error} + */ _getMessageResponseResult(response) { - let error = chrome.runtime.lastError; + const error = chrome.runtime.lastError; if (error) { throw new Error(error.message); } if (!isObject(response)) { throw new Error('Offscreen document did not respond'); } - error = response.error; - if (error) { - throw deserializeError(error); + const error2 = response.error; + if (error2) { + throw ExtensionError.deserialize(error2); } return response.result; } } export class DictionaryDatabaseProxy { + /** + * @param {OffscreenProxy} offscreen + */ constructor(offscreen) { + /** @type {OffscreenProxy} */ this._offscreen = offscreen; } - prepare() { - return this._offscreen.sendMessagePromise({action: 'databasePrepareOffscreen'}); + /** + * @returns {Promise} + */ + async prepare() { + await this._offscreen.sendMessagePromise({action: 'databasePrepareOffscreen'}); } - getDictionaryInfo() { + /** + * @returns {Promise} + */ + async getDictionaryInfo() { return this._offscreen.sendMessagePromise({action: 'getDictionaryInfoOffscreen'}); } - purge() { - return this._offscreen.sendMessagePromise({action: 'databasePurgeOffscreen'}); + /** + * @returns {Promise} + */ + async purge() { + return await this._offscreen.sendMessagePromise({action: 'databasePurgeOffscreen'}); } + /** + * @param {import('dictionary-database').MediaRequest[]} targets + * @returns {Promise} + */ async getMedia(targets) { - const serializedMedia = await this._offscreen.sendMessagePromise({action: 'databaseGetMediaOffscreen', params: {targets}}); + const serializedMedia = /** @type {import('dictionary-database').Media[]} */ (await this._offscreen.sendMessagePromise({action: 'databaseGetMediaOffscreen', params: {targets}})); const media = serializedMedia.map((m) => ({...m, content: ArrayBufferUtil.base64ToArrayBuffer(m.content)})); return media; } } export class TranslatorProxy { + /** + * @param {OffscreenProxy} offscreen + */ constructor(offscreen) { + /** @type {OffscreenProxy} */ this._offscreen = offscreen; } - prepare(deinflectionReasons) { - return this._offscreen.sendMessagePromise({action: 'translatorPrepareOffscreen', params: {deinflectionReasons}}); + /** + * @param {import('deinflector').ReasonsRaw} deinflectionReasons + */ + async prepare(deinflectionReasons) { + await this._offscreen.sendMessagePromise({action: 'translatorPrepareOffscreen', params: {deinflectionReasons}}); } - async findKanji(text, findKanjiOptions) { - const enabledDictionaryMapList = [...findKanjiOptions.enabledDictionaryMap]; - const modifiedKanjiOptions = { - ...findKanjiOptions, + /** + * @param {string} text + * @param {import('translation').FindKanjiOptions} options + * @returns {Promise} + */ + async findKanji(text, options) { + const enabledDictionaryMapList = [...options.enabledDictionaryMap]; + /** @type {import('offscreen').FindKanjiOptionsOffscreen} */ + const modifiedOptions = { + ...options, enabledDictionaryMap: enabledDictionaryMapList }; - return this._offscreen.sendMessagePromise({action: 'findKanjiOffscreen', params: {text, findKanjiOptions: modifiedKanjiOptions}}); + return this._offscreen.sendMessagePromise({action: 'findKanjiOffscreen', params: {text, options: modifiedOptions}}); } - async findTerms(mode, text, findTermsOptions) { - const {enabledDictionaryMap, excludeDictionaryDefinitions, textReplacements} = findTermsOptions; + /** + * @param {import('translator').FindTermsMode} mode + * @param {string} text + * @param {import('translation').FindTermsOptions} options + * @returns {Promise} + */ + async findTerms(mode, text, options) { + const {enabledDictionaryMap, excludeDictionaryDefinitions, textReplacements} = options; const enabledDictionaryMapList = [...enabledDictionaryMap]; const excludeDictionaryDefinitionsList = excludeDictionaryDefinitions ? [...excludeDictionaryDefinitions] : null; const textReplacementsSerialized = textReplacements.map((group) => { - if (!group) { - return group; - } - return group.map((opt) => ({...opt, pattern: opt.pattern.toString()})); + return group !== null ? group.map((opt) => ({...opt, pattern: opt.pattern.toString()})) : null; }); - const modifiedFindTermsOptions = { - ...findTermsOptions, + /** @type {import('offscreen').FindTermsOptionsOffscreen} */ + const modifiedOptions = { + ...options, enabledDictionaryMap: enabledDictionaryMapList, excludeDictionaryDefinitions: excludeDictionaryDefinitionsList, textReplacements: textReplacementsSerialized }; - return this._offscreen.sendMessagePromise({action: 'findTermsOffscreen', params: {mode, text, findTermsOptions: modifiedFindTermsOptions}}); + return this._offscreen.sendMessagePromise({action: 'findTermsOffscreen', params: {mode, text, options: modifiedOptions}}); } + /** + * @param {import('translator').TermReadingList} termReadingList + * @param {string[]} dictionaries + * @returns {Promise} + */ async getTermFrequencies(termReadingList, dictionaries) { return this._offscreen.sendMessagePromise({action: 'getTermFrequenciesOffscreen', params: {termReadingList, dictionaries}}); } - clearDatabaseCaches() { - return this._offscreen.sendMessagePromise({action: 'clearDatabaseCachesOffscreen'}); + /** */ + async clearDatabaseCaches() { + await this._offscreen.sendMessagePromise({action: 'clearDatabaseCachesOffscreen'}); } } @@ -173,7 +236,7 @@ export class ClipboardReaderProxy { set browser(value) { if (this._browser === value) { return; } this._browser = value; - this._offscreen.sendMessagePromise({action: 'clipboardSetBrowserOffsecreen', params: {value}}); + this._offscreen.sendMessagePromise({action: 'clipboardSetBrowserOffscreen', params: {value}}); } /** diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js index 6302aa84..1b68887b 100644 --- a/ext/js/background/offscreen.js +++ b/ext/js/background/offscreen.js @@ -23,7 +23,6 @@ import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; import {DictionaryDatabase} from '../language/dictionary-database.js'; import {JapaneseUtil} from '../language/sandbox/japanese-util.js'; import {Translator} from '../language/translator.js'; -import {yomitan} from '../yomitan.js'; /** * This class controls the core logic of the extension, including API calls @@ -34,12 +33,16 @@ export class Offscreen { * Creates a new instance. */ constructor() { + /** @type {JapaneseUtil} */ this._japaneseUtil = new JapaneseUtil(wanakana); + /** @type {DictionaryDatabase} */ this._dictionaryDatabase = new DictionaryDatabase(); + /** @type {Translator} */ this._translator = new Translator({ japaneseUtil: this._japaneseUtil, database: this._dictionaryDatabase }); + /** @type {ClipboardReader} */ this._clipboardReader = new ClipboardReader({ // eslint-disable-next-line no-undef document: (typeof document === 'object' && document !== null ? document : null), @@ -47,42 +50,45 @@ export class Offscreen { richContentPasteTargetSelector: '#clipboard-rich-content-paste-target' }); + /** @type {import('offscreen').MessageHandlerMap} */ this._messageHandlers = new Map([ - ['clipboardGetTextOffscreen', {async: true, contentScript: true, handler: this._getTextHandler.bind(this)}], - ['clipboardGetImageOffscreen', {async: true, contentScript: true, handler: this._getImageHandler.bind(this)}], - ['clipboardSetBrowserOffsecreen', {async: false, contentScript: true, handler: this._setClipboardBrowser.bind(this)}], - ['databasePrepareOffscreen', {async: true, contentScript: true, handler: this._prepareDatabaseHandler.bind(this)}], - ['getDictionaryInfoOffscreen', {async: true, contentScript: true, handler: this._getDictionaryInfoHandler.bind(this)}], - ['databasePurgeOffscreen', {async: true, contentScript: true, handler: this._purgeDatabaseHandler.bind(this)}], - ['databaseGetMediaOffscreen', {async: true, contentScript: true, handler: this._getMediaHandler.bind(this)}], - ['translatorPrepareOffscreen', {async: false, contentScript: true, handler: this._prepareTranslatorHandler.bind(this)}], - ['findKanjiOffscreen', {async: true, contentScript: true, handler: this._findKanjiHandler.bind(this)}], - ['findTermsOffscreen', {async: true, contentScript: true, handler: this._findTermsHandler.bind(this)}], - ['getTermFrequenciesOffscreen', {async: true, contentScript: true, handler: this._getTermFrequenciesHandler.bind(this)}], - ['clearDatabaseCachesOffscreen', {async: false, contentScript: true, handler: this._clearDatabaseCachesHandler.bind(this)}] + ['clipboardGetTextOffscreen', {async: true, handler: this._getTextHandler.bind(this)}], + ['clipboardGetImageOffscreen', {async: true, handler: this._getImageHandler.bind(this)}], + ['clipboardSetBrowserOffscreen', {async: false, handler: this._setClipboardBrowser.bind(this)}], + ['databasePrepareOffscreen', {async: true, handler: this._prepareDatabaseHandler.bind(this)}], + ['getDictionaryInfoOffscreen', {async: true, handler: this._getDictionaryInfoHandler.bind(this)}], + ['databasePurgeOffscreen', {async: true, handler: this._purgeDatabaseHandler.bind(this)}], + ['databaseGetMediaOffscreen', {async: true, handler: this._getMediaHandler.bind(this)}], + ['translatorPrepareOffscreen', {async: false, handler: this._prepareTranslatorHandler.bind(this)}], + ['findKanjiOffscreen', {async: true, handler: this._findKanjiHandler.bind(this)}], + ['findTermsOffscreen', {async: true, handler: this._findTermsHandler.bind(this)}], + ['getTermFrequenciesOffscreen', {async: true, handler: this._getTermFrequenciesHandler.bind(this)}], + ['clearDatabaseCachesOffscreen', {async: false, handler: this._clearDatabaseCachesHandler.bind(this)}] ]); const onMessage = this._onMessage.bind(this); chrome.runtime.onMessage.addListener(onMessage); + /** @type {?Promise} */ this._prepareDatabasePromise = null; } - _getTextHandler({useRichText}) { - return this._clipboardReader.getText(useRichText); + /** @type {import('offscreen').MessageHandler<'clipboardGetTextOffscreen', true>} */ + async _getTextHandler({useRichText}) { + return await this._clipboardReader.getText(useRichText); } - _getImageHandler() { - return this._clipboardReader.getImage(); + /** @type {import('offscreen').MessageHandler<'clipboardGetImageOffscreen', true>} */ + async _getImageHandler() { + return await this._clipboardReader.getImage(); } - /** - * @param {{value: import('environment').Browser}} details - */ + /** @type {import('offscreen').MessageHandler<'clipboardSetBrowserOffscreen', false>} */ _setClipboardBrowser({value}) { this._clipboardReader.browser = value; } + /** @type {import('offscreen').MessageHandler<'databasePrepareOffscreen', true>} */ _prepareDatabaseHandler() { if (this._prepareDatabasePromise !== null) { return this._prepareDatabasePromise; @@ -91,70 +97,79 @@ export class Offscreen { return this._prepareDatabasePromise; } - _getDictionaryInfoHandler() { - return this._dictionaryDatabase.getDictionaryInfo(); + /** @type {import('offscreen').MessageHandler<'getDictionaryInfoOffscreen', true>} */ + async _getDictionaryInfoHandler() { + return await this._dictionaryDatabase.getDictionaryInfo(); } - _purgeDatabaseHandler() { - return this._dictionaryDatabase.purge(); + /** @type {import('offscreen').MessageHandler<'databasePurgeOffscreen', true>} */ + async _purgeDatabaseHandler() { + return await this._dictionaryDatabase.purge(); } + /** @type {import('offscreen').MessageHandler<'databaseGetMediaOffscreen', true>} */ async _getMediaHandler({targets}) { const media = await this._dictionaryDatabase.getMedia(targets); const serializedMedia = media.map((m) => ({...m, content: ArrayBufferUtil.arrayBufferToBase64(m.content)})); return serializedMedia; } + /** @type {import('offscreen').MessageHandler<'translatorPrepareOffscreen', false>} */ _prepareTranslatorHandler({deinflectionReasons}) { - return this._translator.prepare(deinflectionReasons); + this._translator.prepare(deinflectionReasons); } - _findKanjiHandler({text, findKanjiOptions}) { - findKanjiOptions.enabledDictionaryMap = new Map(findKanjiOptions.enabledDictionaryMap); - return this._translator.findKanji(text, findKanjiOptions); + /** @type {import('offscreen').MessageHandler<'findKanjiOffscreen', true>} */ + async _findKanjiHandler({text, options}) { + /** @type {import('translation').FindKanjiOptions} */ + const modifiedOptions = { + ...options, + enabledDictionaryMap: new Map(options.enabledDictionaryMap) + }; + return await this._translator.findKanji(text, modifiedOptions); } - _findTermsHandler({mode, text, findTermsOptions}) { - findTermsOptions.enabledDictionaryMap = new Map(findTermsOptions.enabledDictionaryMap); - if (findTermsOptions.excludeDictionaryDefinitions) { - findTermsOptions.excludeDictionaryDefinitions = new Set(findTermsOptions.excludeDictionaryDefinitions); - } - findTermsOptions.textReplacements = findTermsOptions.textReplacements.map((group) => { - if (!group) { - return group; - } + /** @type {import('offscreen').MessageHandler<'findTermsOffscreen', true>} */ + _findTermsHandler({mode, text, options}) { + const enabledDictionaryMap = new Map(options.enabledDictionaryMap); + const excludeDictionaryDefinitions = ( + options.excludeDictionaryDefinitions !== null ? + new Set(options.excludeDictionaryDefinitions) : + null + ); + const textReplacements = options.textReplacements.map((group) => { + if (group === null) { return null; } return group.map((opt) => { - const [, pattern, flags] = opt.pattern.match(/\/(.*?)\/([a-z]*)?$/i); // https://stackoverflow.com/a/33642463 + // https://stackoverflow.com/a/33642463 + const match = opt.pattern.match(/\/(.*?)\/([a-z]*)?$/i); + const [, pattern, flags] = match !== null ? match : ['', '', '']; return {...opt, pattern: new RegExp(pattern, flags ?? '')}; }); }); - return this._translator.findTerms(mode, text, findTermsOptions); + /** @type {import('translation').FindTermsOptions} */ + const modifiedOptions = { + ...options, + enabledDictionaryMap, + excludeDictionaryDefinitions, + textReplacements + }; + return this._translator.findTerms(mode, text, modifiedOptions); } + /** @type {import('offscreen').MessageHandler<'getTermFrequenciesOffscreen', true>} */ _getTermFrequenciesHandler({termReadingList, dictionaries}) { return this._translator.getTermFrequencies(termReadingList, dictionaries); } + /** @type {import('offscreen').MessageHandler<'clearDatabaseCachesOffscreen', false>} */ _clearDatabaseCachesHandler() { - return this._translator.clearDatabaseCaches(); + this._translator.clearDatabaseCaches(); } + /** @type {import('extension').ChromeRuntimeOnMessageCallback} */ _onMessage({action, params}, sender, callback) { const messageHandler = this._messageHandlers.get(action); if (typeof messageHandler === 'undefined') { return false; } - this._validatePrivilegedMessageSender(sender); - return invokeMessageHandler(messageHandler, params, callback, sender); } - - _validatePrivilegedMessageSender(sender) { - let {url} = sender; - if (typeof url === 'string' && yomitan.isExtensionUrl(url)) { return; } - const {tab} = url; - if (typeof tab === 'object' && tab !== null) { - ({url} = tab); - if (typeof url === 'string' && yomitan.isExtensionUrl(url)) { return; } - } - throw new Error('Invalid message sender'); - } } diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index c21b16b1..aa1b71dd 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -157,7 +157,7 @@ export class Translator { /** * Gets a list of frequency information for a given list of term-reading pairs * and a list of dictionaries. - * @param {{term: string, reading: string|null}[]} termReadingList An array of `{term, reading}` pairs. If reading is null, + * @param {import('translator').TermReadingList} termReadingList An array of `{term, reading}` pairs. If reading is null, * the reading won't be compared. * @param {string[]} dictionaries An array of dictionary names. * @returns {Promise} An array of term frequencies. @@ -203,7 +203,7 @@ export class Translator { * @param {Map} enabledDictionaryMap * @param {import('translation').FindTermsOptions} options * @param {TranslatorTagAggregator} tagAggregator - * @returns {Promise<{dictionaryEntries: import('dictionary').TermDictionaryEntry[], originalTextLength: number}>} + * @returns {Promise} */ async _findTermsInternal(text, enabledDictionaryMap, options, tagAggregator) { if (options.removeNonJapaneseCharacters) { diff --git a/types/ext/dictionary-database.d.ts b/types/ext/dictionary-database.d.ts index 2e0f854b..06a246e8 100644 --- a/types/ext/dictionary-database.d.ts +++ b/types/ext/dictionary-database.d.ts @@ -35,7 +35,7 @@ export interface MediaDataBase { export interface MediaDataArrayBufferContent extends MediaDataBase {} export interface MediaDataStringContent extends MediaDataBase {} -export type Media = {index: number} & MediaDataBase; +export type Media = {index: number} & MediaDataBase; export type DatabaseTermEntry = { expression: string; diff --git a/types/ext/offscreen.d.ts b/types/ext/offscreen.d.ts new file mode 100644 index 00000000..7dd64d1e --- /dev/null +++ b/types/ext/offscreen.d.ts @@ -0,0 +1,115 @@ +/* + * 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 . + */ + +import type * as Core from './core'; +import type * as Deinflector from './deinflector'; +import type * as Dictionary from './dictionary'; +import type * as DictionaryDatabase from './dictionary-database'; +import type * as DictionaryImporter from './dictionary-importer'; +import type * as Environment from './environment'; +import type * as Translation from './translation'; +import type * as Translator from './translator'; + +export type MessageAny2 = Message; + +export type Message = ( + MessageDetailsMap[T] extends undefined ? + {action: T} : + {action: T, params: MessageDetailsMap[T]} +); + +export type MessageReturn = MessageReturnMap[T]; + +type MessageDetailsMap = { + databasePrepareOffscreen: undefined; + getDictionaryInfoOffscreen: undefined; + databasePurgeOffscreen: undefined; + databaseGetMediaOffscreen: { + targets: DictionaryDatabase.MediaRequest[]; + }; + translatorPrepareOffscreen: { + deinflectionReasons: Deinflector.ReasonsRaw; + }; + findKanjiOffscreen: { + text: string; + options: FindKanjiOptionsOffscreen; + }; + findTermsOffscreen: { + mode: Translator.FindTermsMode; + text: string; + options: FindTermsOptionsOffscreen; + }; + getTermFrequenciesOffscreen: { + termReadingList: Translator.TermReadingList; + dictionaries: string[]; + }; + clearDatabaseCachesOffscreen: undefined; + clipboardSetBrowserOffscreen: { + value: Environment.Browser | null; + }; + clipboardGetTextOffscreen: { + useRichText: boolean; + }; + clipboardGetImageOffscreen: undefined; +}; + +type MessageReturnMap = { + databasePrepareOffscreen: void; + getDictionaryInfoOffscreen: DictionaryImporter.Summary[]; + databasePurgeOffscreen: boolean; + databaseGetMediaOffscreen: DictionaryDatabase.Media[]; + translatorPrepareOffscreen: void; + findKanjiOffscreen: Dictionary.KanjiDictionaryEntry[]; + findTermsOffscreen: Translator.FindTermsResult; + getTermFrequenciesOffscreen: Translator.TermFrequencySimple[]; + clearDatabaseCachesOffscreen: void; + clipboardSetBrowserOffscreen: void; + clipboardGetTextOffscreen: string; + clipboardGetImageOffscreen: string | null; +}; + +export type MessageType = keyof MessageDetailsMap; + +export type FindKanjiOptionsOffscreen = Omit & { + enabledDictionaryMap: [ + key: string, + options: Translation.FindKanjiDictionary, + ][]; +}; + + +export type FindTermsOptionsOffscreen = Omit & { + enabledDictionaryMap: [ + key: string, + options: Translation.FindTermDictionary, + ][]; + excludeDictionaryDefinitions: string[] | null; + textReplacements: (FindTermsTextReplacementOffscreen[] | null)[]; +}; + +export type FindTermsTextReplacementOffscreen = Omit & { + pattern: string; +}; + +export type MessageHandler< + TMessage extends MessageType, + TIsAsync extends boolean, +> = ( + details: MessageDetailsMap[TMessage], +) => (TIsAsync extends true ? Promise> : MessageReturn); + +export type MessageHandlerMap = Map; diff --git a/types/ext/translator.d.ts b/types/ext/translator.d.ts index f17b3bf6..e7d45295 100644 --- a/types/ext/translator.d.ts +++ b/types/ext/translator.d.ts @@ -81,3 +81,15 @@ export type SequenceQuery = { }; export type FindTermsMode = 'simple' | 'group' | 'merge' | 'split'; + +export type TermReadingItem = { + term: string; + reading: string | null; +}; + +export type TermReadingList = TermReadingItem[]; + +export type FindTermsResult = { + dictionaryEntries: Dictionary.TermDictionaryEntry[]; + originalTextLength: number; +}; -- cgit v1.2.3