From 4d035e33a1d3f8ad646f4907417aa94b7f78c818 Mon Sep 17 00:00:00 2001 From: Cashew Date: Sun, 10 Dec 2023 00:38:43 +0900 Subject: [PATCH 01/12] lesen-tan initial commit --- ext/css/display.css | 140 +++++ ext/js/background/backend.js | 688 +++++++++++++------------ ext/js/display/display.js | 390 ++++++++++---- ext/js/language/dictionary-database.js | 61 +-- ext/js/language/text-scanner.js | 154 ++++-- ext/js/language/translator.js | 390 ++++++++++---- ext/search.html | 3 +- types/ext/dictionary-database.d.ts | 2 + types/ext/translation-internal.d.ts | 2 + 9 files changed, 1199 insertions(+), 631 deletions(-) diff --git a/ext/css/display.css b/ext/css/display.css index 49aeaaa5d0..f29bb943b0 100644 --- a/ext/css/display.css +++ b/ext/css/display.css @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2016-2022 Yomichan Authors * @@ -1952,3 +1953,142 @@ button.footer-notification-close-button { :root[data-popup-display-mode=full-width] .frame-resizer-container { display: none; } + +/* https://github.com/seth-js/yomichan-de */ +/* Seth's Custom CSS */ +.gloss-list { + list-style-type: decimal; +} + +.tag[data-category='dictionary'] { + display: none; +} + +.tag[data-category='masculine'] { + --tag-color: #0070ff; +} + +.tag[data-category='feminine'] { + --tag-color: #ec3654; +} + +.tag[data-category='neuter'] { + --tag-color: #269100; +} + +.definition-tag-list { + display: inline; +} + +.inflection { + color: var(--reason-text-color); + font-size: 12px; + font-style: italic; +} + +.inflection-list { + display: none; +} + +.inflection-separator+.inflection::before { + content: var(--inflection-separator); + padding: 0 0.25em; +} + +div[data-section-type='frequencies'] { + display: none; +} + +:root[data-theme='dark'] { + --text-color: white; +} + +.headword-term { + font-weight: 300; +} + +.entry { + padding: 30px 0; +} + +*::-webkit-scrollbar { + width: 5px !important; +} + +*::-webkit-scrollbar-thumb { + /* background: #777 !important; */ + border-radius: 20px !important; +} + +* { + /* scrollbar-color: #777 #000 !important; */ + scrollbar-width: thin !important; +} + +.gloss-list { + margin-top: 10px; +} + +.gloss-item { + margin-bottom: 7px; +} + +.entry-header { + margin-bottom: 10px; +} + +.form-info-box { + display: none; + padding: 20px; + width: max-content; + box-shadow: 0 1px 12px rgb(0 0 0 / 12%), 0 1px 4px rgb(0 0 0 / 24%); + z-index: 1; + margin-top: 10px; +} + +:root[data-theme="dark"] .form-info-box { + border: 1px solid #333; + background-color: #1e1e1e; +} + +.form-info-box ol { + padding: 0; +} + +.form-info-box li { + list-style-position: inside; +} + +.show-info-btn { + width: max-content; + user-select: none; +} + +.show-info-btn:hover { + cursor: help; +} + +/* ruby { + display: inline-flex; + flex-direction: column-reverse; +} */ + +.pointer-text { + margin-bottom: 5px; + font-style: italic; +} + +.automated-result-text { + font-size: 0.9em; + text-align: right; + margin-bottom: 5px; +} + +.definition-list { + list-style-type: none; + padding: 0; +} + +html[data-page-type="popup"] .entry { + padding: 30px; +} diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 3eefed538a..21cc2de2c4 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -17,29 +17,29 @@ */ import * as wanakana from '../../lib/wanakana.js'; -import {AccessibilityController} from '../accessibility/accessibility-controller.js'; -import {AnkiConnect} from '../comm/anki-connect.js'; -import {ClipboardMonitor} from '../comm/clipboard-monitor.js'; -import {ClipboardReader} from '../comm/clipboard-reader.js'; -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 {OptionsUtil} from '../data/options-util.js'; -import {PermissionsUtil} from '../data/permissions-util.js'; -import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; -import {Environment} from '../extension/environment.js'; -import {ObjectPropertyAccessor} from '../general/object-property-accessor.js'; -import {DictionaryDatabase} from '../language/dictionary-database.js'; -import {JapaneseUtil} from '../language/sandbox/japanese-util.js'; -import {Translator} from '../language/translator.js'; -import {AudioDownloader} from '../media/audio-downloader.js'; -import {MediaUtil} from '../media/media-util.js'; -import {yomitan} from '../yomitan.js'; -import {ClipboardReaderProxy, DictionaryDatabaseProxy, OffscreenProxy, TranslatorProxy} from './offscreen-proxy.js'; -import {ProfileConditionsUtil} from './profile-conditions-util.js'; -import {RequestBuilder} from './request-builder.js'; -import {ScriptManager} from './script-manager.js'; +import { AccessibilityController } from '../accessibility/accessibility-controller.js'; +import { AnkiConnect } from '../comm/anki-connect.js'; +import { ClipboardMonitor } from '../comm/clipboard-monitor.js'; +import { ClipboardReader } from '../comm/clipboard-reader.js'; +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 { OptionsUtil } from '../data/options-util.js'; +import { PermissionsUtil } from '../data/permissions-util.js'; +import { ArrayBufferUtil } from '../data/sandbox/array-buffer-util.js'; +import { Environment } from '../extension/environment.js'; +import { ObjectPropertyAccessor } from '../general/object-property-accessor.js'; +import { DictionaryDatabase } from '../language/dictionary-database.js'; +import { JapaneseUtil } from '../language/sandbox/japanese-util.js'; +import { Translator } from '../language/translator.js'; +import { AudioDownloader } from '../media/audio-downloader.js'; +import { MediaUtil } from '../media/media-util.js'; +import { yomitan } from '../yomitan.js'; +import { ClipboardReaderProxy, DictionaryDatabaseProxy, OffscreenProxy, TranslatorProxy } from './offscreen-proxy.js'; +import { ProfileConditionsUtil } from './profile-conditions-util.js'; +import { RequestBuilder } from './request-builder.js'; +import { ScriptManager } from './script-manager.js'; /** * This class controls the core logic of the extension, including API calls @@ -128,7 +128,7 @@ export class Backend { /** @type {?Promise} */ this._preparePromise = null; /** @type {import('core').DeferredPromiseDetails} */ - const {promise, resolve, reject} = deferPromise(); + const { promise, resolve, reject } = deferPromise(); /** @type {Promise} */ this._prepareCompletePromise = promise; /** @type {() => void} */ @@ -148,63 +148,63 @@ export class Backend { this._permissionsUtil = new PermissionsUtil(); /** @type {import('backend').MessageHandlerMap} */ - this._messageHandlers = new Map(/** @type {import('backend').MessageHandlerMapInit} */ ([ - ['requestBackendReadySignal', {async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this)}], - ['optionsGet', {async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this)}], - ['optionsGetFull', {async: false, contentScript: true, handler: this._onApiOptionsGetFull.bind(this)}], - ['kanjiFind', {async: true, contentScript: true, handler: this._onApiKanjiFind.bind(this)}], - ['termsFind', {async: true, contentScript: true, handler: this._onApiTermsFind.bind(this)}], - ['parseText', {async: true, contentScript: true, handler: this._onApiParseText.bind(this)}], - ['getAnkiConnectVersion', {async: true, contentScript: true, handler: this._onApiGetAnkiConnectVersion.bind(this)}], - ['isAnkiConnected', {async: true, contentScript: true, handler: this._onApiIsAnkiConnected.bind(this)}], - ['addAnkiNote', {async: true, contentScript: true, handler: this._onApiAddAnkiNote.bind(this)}], - ['getAnkiNoteInfo', {async: true, contentScript: true, handler: this._onApiGetAnkiNoteInfo.bind(this)}], - ['injectAnkiNoteMedia', {async: true, contentScript: true, handler: this._onApiInjectAnkiNoteMedia.bind(this)}], - ['noteView', {async: true, contentScript: true, handler: this._onApiNoteView.bind(this)}], - ['suspendAnkiCardsForNote', {async: true, contentScript: true, handler: this._onApiSuspendAnkiCardsForNote.bind(this)}], - ['commandExec', {async: false, contentScript: true, handler: this._onApiCommandExec.bind(this)}], - ['getTermAudioInfoList', {async: true, contentScript: true, handler: this._onApiGetTermAudioInfoList.bind(this)}], - ['sendMessageToFrame', {async: false, contentScript: true, handler: this._onApiSendMessageToFrame.bind(this)}], - ['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}], - ['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}], - ['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}], - ['getStylesheetContent', {async: true, contentScript: true, handler: this._onApiGetStylesheetContent.bind(this)}], - ['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}], - ['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}], - ['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}], - ['getZoom', {async: true, contentScript: true, handler: this._onApiGetZoom.bind(this)}], - ['getDefaultAnkiFieldTemplates', {async: false, contentScript: true, handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this)}], - ['getDictionaryInfo', {async: true, contentScript: true, handler: this._onApiGetDictionaryInfo.bind(this)}], - ['purgeDatabase', {async: true, contentScript: false, handler: this._onApiPurgeDatabase.bind(this)}], - ['getMedia', {async: true, contentScript: true, handler: this._onApiGetMedia.bind(this)}], - ['log', {async: false, contentScript: true, handler: this._onApiLog.bind(this)}], - ['logIndicatorClear', {async: false, contentScript: true, handler: this._onApiLogIndicatorClear.bind(this)}], - ['createActionPort', {async: false, contentScript: true, handler: this._onApiCreateActionPort.bind(this)}], - ['modifySettings', {async: true, contentScript: true, handler: this._onApiModifySettings.bind(this)}], - ['getSettings', {async: false, contentScript: true, handler: this._onApiGetSettings.bind(this)}], - ['setAllSettings', {async: true, contentScript: false, handler: this._onApiSetAllSettings.bind(this)}], - ['getOrCreateSearchPopup', {async: true, contentScript: true, handler: this._onApiGetOrCreateSearchPopup.bind(this)}], - ['isTabSearchPopup', {async: true, contentScript: true, handler: this._onApiIsTabSearchPopup.bind(this)}], - ['triggerDatabaseUpdated', {async: false, contentScript: true, handler: this._onApiTriggerDatabaseUpdated.bind(this)}], - ['testMecab', {async: true, contentScript: true, handler: this._onApiTestMecab.bind(this)}], - ['textHasJapaneseCharacters', {async: false, contentScript: true, handler: this._onApiTextHasJapaneseCharacters.bind(this)}], - ['getTermFrequencies', {async: true, contentScript: true, handler: this._onApiGetTermFrequencies.bind(this)}], - ['findAnkiNotes', {async: true, contentScript: true, handler: this._onApiFindAnkiNotes.bind(this)}], - ['loadExtensionScripts', {async: true, contentScript: true, handler: this._onApiLoadExtensionScripts.bind(this)}], - ['openCrossFramePort', {async: false, contentScript: true, handler: this._onApiOpenCrossFramePort.bind(this)}] + this._messageHandlers = new Map(/** @type {import('backend').MessageHandlerMapInit} */([ + ['requestBackendReadySignal', { async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this) }], + ['optionsGet', { async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this) }], + ['optionsGetFull', { async: false, contentScript: true, handler: this._onApiOptionsGetFull.bind(this) }], + ['kanjiFind', { async: true, contentScript: true, handler: this._onApiKanjiFind.bind(this) }], + ['termsFind', { async: true, contentScript: true, handler: this._onApiTermsFind.bind(this) }], + ['parseText', { async: true, contentScript: true, handler: this._onApiParseText.bind(this) }], + ['getAnkiConnectVersion', { async: true, contentScript: true, handler: this._onApiGetAnkiConnectVersion.bind(this) }], + ['isAnkiConnected', { async: true, contentScript: true, handler: this._onApiIsAnkiConnected.bind(this) }], + ['addAnkiNote', { async: true, contentScript: true, handler: this._onApiAddAnkiNote.bind(this) }], + ['getAnkiNoteInfo', { async: true, contentScript: true, handler: this._onApiGetAnkiNoteInfo.bind(this) }], + ['injectAnkiNoteMedia', { async: true, contentScript: true, handler: this._onApiInjectAnkiNoteMedia.bind(this) }], + ['noteView', { async: true, contentScript: true, handler: this._onApiNoteView.bind(this) }], + ['suspendAnkiCardsForNote', { async: true, contentScript: true, handler: this._onApiSuspendAnkiCardsForNote.bind(this) }], + ['commandExec', { async: false, contentScript: true, handler: this._onApiCommandExec.bind(this) }], + ['getTermAudioInfoList', { async: true, contentScript: true, handler: this._onApiGetTermAudioInfoList.bind(this) }], + ['sendMessageToFrame', { async: false, contentScript: true, handler: this._onApiSendMessageToFrame.bind(this) }], + ['broadcastTab', { async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this) }], + ['frameInformationGet', { async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this) }], + ['injectStylesheet', { async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this) }], + ['getStylesheetContent', { async: true, contentScript: true, handler: this._onApiGetStylesheetContent.bind(this) }], + ['getEnvironmentInfo', { async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this) }], + ['clipboardGet', { async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this) }], + ['getDisplayTemplatesHtml', { async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this) }], + ['getZoom', { async: true, contentScript: true, handler: this._onApiGetZoom.bind(this) }], + ['getDefaultAnkiFieldTemplates', { async: false, contentScript: true, handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this) }], + ['getDictionaryInfo', { async: true, contentScript: true, handler: this._onApiGetDictionaryInfo.bind(this) }], + ['purgeDatabase', { async: true, contentScript: false, handler: this._onApiPurgeDatabase.bind(this) }], + ['getMedia', { async: true, contentScript: true, handler: this._onApiGetMedia.bind(this) }], + ['log', { async: false, contentScript: true, handler: this._onApiLog.bind(this) }], + ['logIndicatorClear', { async: false, contentScript: true, handler: this._onApiLogIndicatorClear.bind(this) }], + ['createActionPort', { async: false, contentScript: true, handler: this._onApiCreateActionPort.bind(this) }], + ['modifySettings', { async: true, contentScript: true, handler: this._onApiModifySettings.bind(this) }], + ['getSettings', { async: false, contentScript: true, handler: this._onApiGetSettings.bind(this) }], + ['setAllSettings', { async: true, contentScript: false, handler: this._onApiSetAllSettings.bind(this) }], + ['getOrCreateSearchPopup', { async: true, contentScript: true, handler: this._onApiGetOrCreateSearchPopup.bind(this) }], + ['isTabSearchPopup', { async: true, contentScript: true, handler: this._onApiIsTabSearchPopup.bind(this) }], + ['triggerDatabaseUpdated', { async: false, contentScript: true, handler: this._onApiTriggerDatabaseUpdated.bind(this) }], + ['testMecab', { async: true, contentScript: true, handler: this._onApiTestMecab.bind(this) }], + ['textHasJapaneseCharacters', { async: false, contentScript: true, handler: this._onApiTextHasJapaneseCharacters.bind(this) }], + ['getTermFrequencies', { async: true, contentScript: true, handler: this._onApiGetTermFrequencies.bind(this) }], + ['findAnkiNotes', { async: true, contentScript: true, handler: this._onApiFindAnkiNotes.bind(this) }], + ['loadExtensionScripts', { async: true, contentScript: true, handler: this._onApiLoadExtensionScripts.bind(this) }], + ['openCrossFramePort', { async: false, contentScript: true, handler: this._onApiOpenCrossFramePort.bind(this) }] ])); /** @type {import('backend').MessageHandlerWithProgressMap} */ - this._messageHandlersWithProgress = new Map(/** @type {import('backend').MessageHandlerWithProgressMapInit} */ ([ + this._messageHandlersWithProgress = new Map(/** @type {import('backend').MessageHandlerWithProgressMapInit} */([ // Empty ])); /** @type {Map void>} */ - this._commandHandlers = new Map(/** @type {[name: string, handler: (params?: import('core').SerializableObject) => void][]} */ ([ + this._commandHandlers = new Map(/** @type {[name: string, handler: (params?: import('core').SerializableObject) => void][]} */([ ['toggleTextScanning', this._onCommandToggleTextScanning.bind(this)], - ['openInfoPage', this._onCommandOpenInfoPage.bind(this)], - ['openSettingsPage', this._onCommandOpenSettingsPage.bind(this)], - ['openSearchPage', this._onCommandOpenSearchPage.bind(this)], - ['openPopupWindow', this._onCommandOpenPopupWindow.bind(this)] + ['openInfoPage', this._onCommandOpenInfoPage.bind(this)], + ['openSettingsPage', this._onCommandOpenSettingsPage.bind(this)], + ['openSearchPage', this._onCommandOpenSearchPage.bind(this)], + ['openPopupWindow', this._onCommandOpenPopupWindow.bind(this)] ])); } @@ -298,7 +298,7 @@ export class Backend { this._applyOptions('background'); - const options = this._getProfileOptions({current: true}, false); + const options = this._getProfileOptions({ current: true }, false); if (options.general.showGuide) { this._openWelcomeGuidePageOnce(); } @@ -306,7 +306,7 @@ export class Backend { this._clipboardMonitor.on('change', this._onClipboardTextChange.bind(this)); this._sendMessageAllTabsIgnoreResponse('Yomitan.backendReady', {}); - this._sendMessageIgnoreResponse({action: 'Yomitan.backendReady', params: {}}); + this._sendMessageIgnoreResponse({ action: 'Yomitan.backendReady', params: {} }); } catch (e) { log.error(e); throw e; @@ -323,14 +323,14 @@ export class Backend { /** * @param {{text: string}} params */ - async _onClipboardTextChange({text}) { - const {clipboard: {maximumSearchLength}} = this._getProfileOptions({current: true}, false); + async _onClipboardTextChange({ text }) { + const { clipboard: { maximumSearchLength } } = this._getProfileOptions({ current: true }, false); if (text.length > maximumSearchLength) { text = text.substring(0, maximumSearchLength); } try { - const {tab, created} = await this._getOrCreateSearchPopup(); - const {id} = tab; + const { tab, created } = await this._getOrCreateSearchPopup(); + const { id } = tab; if (typeof id !== 'number') { throw new Error('Tab does not have an id'); } @@ -344,7 +344,7 @@ export class Backend { /** * @param {{level: import('log').LogLevel}} params */ - _onLog({level}) { + _onLog({ level }) { const levelValue = this._getErrorLevelValue(level); if (levelValue <= this._getErrorLevelValue(this._logErrorLevel)) { return; } @@ -368,7 +368,7 @@ export class Backend { this._prepareCompletePromise.then( () => { handler(...args); }, - () => {} // NOP + () => { } // NOP ); }); } @@ -401,7 +401,7 @@ export class Backend { * @param {(response?: unknown) => void} callback * @returns {boolean} */ - _onMessage({action, params}, sender, callback) { + _onMessage({ action, params }, sender, callback) { const messageHandler = this._messageHandlers.get(action); if (typeof messageHandler === 'undefined') { return false; } @@ -409,7 +409,7 @@ export class Backend { try { this._validatePrivilegedMessageSender(sender); } catch (error) { - callback({error: ExtensionError.serialize(error)}); + callback({ error: ExtensionError.serialize(error) }); return false; } } @@ -420,8 +420,8 @@ export class Backend { /** * @param {chrome.tabs.ZoomChangeInfo} event */ - _onZoomChange({tabId, oldZoomFactor, newZoomFactor}) { - this._sendMessageTabIgnoreResponse(tabId, {action: 'Yomitan.zoomChanged', params: {oldZoomFactor, newZoomFactor}}, {}); + _onZoomChange({ tabId, oldZoomFactor, newZoomFactor }) { + this._sendMessageTabIgnoreResponse(tabId, { action: 'Yomitan.zoomChanged', params: { oldZoomFactor, newZoomFactor } }, {}); } /** @@ -434,7 +434,7 @@ export class Backend { /** * @param {chrome.runtime.InstalledDetails} event */ - _onInstalled({reason}) { + _onInstalled({ reason }) { if (reason !== 'install') { return; } this._requestPersistentStorage(); } @@ -444,12 +444,12 @@ export class Backend { /** @type {import('api').Handler} */ _onApiRequestBackendReadySignal(_params, sender) { // tab ID isn't set in background (e.g. browser_action) - const data = {action: 'Yomitan.backendReady', params: {}}; + const data = { action: 'Yomitan.backendReady', params: {} }; if (typeof sender.tab === 'undefined') { this._sendMessageIgnoreResponse(data); return false; } else { - const {id} = sender.tab; + const { id } = sender.tab; if (typeof id === 'number') { this._sendMessageTabIgnoreResponse(id, data, {}); } @@ -458,7 +458,7 @@ export class Backend { } /** @type {import('api').Handler} */ - _onApiOptionsGet({optionsContext}) { + _onApiOptionsGet({ optionsContext }) { return this._getProfileOptions(optionsContext, false); } @@ -468,9 +468,9 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiKanjiFind({text, optionsContext}) { + async _onApiKanjiFind({ text, optionsContext }) { const options = this._getProfileOptions(optionsContext, false); - const {general: {maxResults}} = options; + const { general: { maxResults } } = options; const findKanjiOptions = this._getTranslatorFindKanjiOptions(options); const dictionaryEntries = await this._translator.findKanji(text, findKanjiOptions); dictionaryEntries.splice(maxResults); @@ -478,17 +478,17 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiTermsFind({text, details, optionsContext}) { + async _onApiTermsFind({ text, details, optionsContext }) { const options = this._getProfileOptions(optionsContext, false); - const {general: {resultOutputMode: mode, maxResults}} = options; + const { general: { resultOutputMode: mode, maxResults } } = options; const findTermsOptions = this._getTranslatorFindTermsOptions(mode, details, options); - const {dictionaryEntries, originalTextLength} = await this._translator.findTerms(mode, text, findTermsOptions); + const { dictionaryEntries, originalTextLength } = await this._translator.findTerms(mode, text, findTermsOptions); dictionaryEntries.splice(maxResults); - return {dictionaryEntries, originalTextLength}; + return { dictionaryEntries, originalTextLength }; } /** @type {import('api').Handler} */ - async _onApiParseText({text, optionsContext, scanLength, useInternalParser, useMecabParser}) { + async _onApiParseText({ text, optionsContext, scanLength, useInternalParser, useMecabParser }) { const [internalResults, mecabResults] = await Promise.all([ (useInternalParser ? this._textParseScanning(text, scanLength, optionsContext) : null), (useMecabParser ? this._textParseMecab(text) : null) @@ -531,12 +531,12 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiAddAnkiNote({note}) { + async _onApiAddAnkiNote({ note }) { return await this._anki.addNote(note); } /** @type {import('api').Handler} */ - async _onApiGetAnkiNoteInfo({notes, fetchAdditionalInfo}) { + async _onApiGetAnkiNoteInfo({ notes, fetchAdditionalInfo }) { /** @type {import('anki').NoteInfoWrapper[]} */ const results = []; /** @type {{note: import('anki').Note, info: import('anki').NoteInfoWrapper}[]} */ @@ -548,15 +548,15 @@ export class Backend { let canAdd = canAddArray[i]; const valid = AnkiUtil.isNoteDataValid(note); if (!valid) { canAdd = false; } - const info = {canAdd, valid, noteIds: null}; + const info = { canAdd, valid, noteIds: null }; results.push(info); if (!canAdd && valid) { - cannotAdd.push({note, info}); + cannotAdd.push({ note, info }); } } if (cannotAdd.length > 0) { - const cannotAddNotes = cannotAdd.map(({note}) => note); + const cannotAddNotes = cannotAdd.map(({ note }) => note); const noteIdsArray = await this._anki.findNoteIds(cannotAddNotes); for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) { const noteIds = noteIdsArray[i]; @@ -573,7 +573,7 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiInjectAnkiNoteMedia({timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}) { + async _onApiInjectAnkiNoteMedia({ timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails }) { return await this._injectAnkNoteMedia( this._anki, timestamp, @@ -586,7 +586,7 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiNoteView({noteId, mode, allowFallback}) { + async _onApiNoteView({ noteId, mode, allowFallback }) { if (mode === 'edit') { try { await this._anki.guiEditNote(noteId); @@ -605,7 +605,7 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiSuspendAnkiCardsForNote({noteId}) { + async _onApiSuspendAnkiCardsForNote({ noteId }) { const cardIds = await this._anki.findCardsForNote(noteId); const count = cardIds.length; if (count > 0) { @@ -616,39 +616,39 @@ export class Backend { } /** @type {import('api').Handler} */ - _onApiCommandExec({command, params}) { + _onApiCommandExec({ command, params }) { return this._runCommand(command, params); } /** @type {import('api').Handler} */ - async _onApiGetTermAudioInfoList({source, term, reading}) { + async _onApiGetTermAudioInfoList({ source, term, reading }) { return await this._audioDownloader.getTermAudioInfoList(source, term, reading); } /** @type {import('api').Handler} */ - _onApiSendMessageToFrame({frameId: targetFrameId, action, params}, sender) { + _onApiSendMessageToFrame({ frameId: targetFrameId, action, params }, sender) { if (!sender) { return false; } - const {tab} = sender; + const { tab } = sender; if (!tab) { return false; } - const {id} = tab; + const { id } = tab; if (typeof id !== 'number') { return false; } const frameId = sender.frameId; /** @type {import('extension').ChromeRuntimeMessageWithFrameId} */ - const message = {action, params, frameId}; - this._sendMessageTabIgnoreResponse(id, message, {frameId: targetFrameId}); + const message = { action, params, frameId }; + this._sendMessageTabIgnoreResponse(id, message, { frameId: targetFrameId }); return true; } /** @type {import('api').Handler} */ - _onApiBroadcastTab({action, params}, sender) { + _onApiBroadcastTab({ action, params }, sender) { if (!sender) { return false; } - const {tab} = sender; + const { tab } = sender; if (!tab) { return false; } - const {id} = tab; + const { id } = tab; if (typeof id !== 'number') { return false; } const frameId = sender.frameId; /** @type {import('extension').ChromeRuntimeMessageWithFrameId} */ - const message = {action, params, frameId}; + const message = { action, params, frameId }; this._sendMessageTabIgnoreResponse(id, message, {}); return true; } @@ -658,18 +658,18 @@ export class Backend { const tab = sender.tab; const tabId = tab ? tab.id : void 0; const frameId = sender.frameId; - return Promise.resolve({tabId, frameId}); + return Promise.resolve({ tabId, frameId }); } /** @type {import('api').Handler} */ - async _onApiInjectStylesheet({type, value}, sender) { - const {frameId, tab} = sender; + 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); } /** @type {import('api').Handler} */ - async _onApiGetStylesheetContent({url}) { + async _onApiGetStylesheetContent({ url }) { if (!url.startsWith('/') || url.startsWith('//') || !url.endsWith('.css')) { throw new Error('Invalid URL'); } @@ -707,7 +707,7 @@ export class Backend { typeof chrome.tabs.getZoom === 'function' )) { // Not supported - resolve({zoomFactor: 1.0}); + resolve({ zoomFactor: 1.0 }); return; } chrome.tabs.getZoom(tabId, (zoomFactor) => { @@ -715,7 +715,7 @@ export class Backend { if (e) { reject(new Error(e.message)); } else { - resolve({zoomFactor}); + resolve({ zoomFactor }); } }); }); @@ -738,12 +738,12 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiGetMedia({targets}) { + async _onApiGetMedia({ targets }) { return await this._getNormalizedDictionaryDatabaseMedia(targets); } /** @type {import('api').Handler} */ - _onApiLog({error, level, context}) { + _onApiLog({ error, level, context }) { log.log(ExtensionError.deserialize(error), level, context); } @@ -767,7 +767,7 @@ export class Backend { id }; - const port = chrome.tabs.connect(tabId, {name: JSON.stringify(details), frameId}); + const port = chrome.tabs.connect(tabId, { name: JSON.stringify(details), frameId }); try { this._createActionListenerPort(port, sender, this._messageHandlersWithProgress); } catch (e) { @@ -779,56 +779,56 @@ export class Backend { } /** @type {import('api').Handler} */ - _onApiModifySettings({targets, source}) { + _onApiModifySettings({ targets, source }) { return this._modifySettings(targets, source); } /** @type {import('api').Handler} */ - _onApiGetSettings({targets}) { + _onApiGetSettings({ targets }) { const results = []; for (const target of targets) { try { const result = this._getSetting(target); - results.push({result: clone(result)}); + results.push({ result: clone(result) }); } catch (e) { - results.push({error: ExtensionError.serialize(e)}); + results.push({ error: ExtensionError.serialize(e) }); } } return results; } /** @type {import('api').Handler} */ - async _onApiSetAllSettings({value, source}) { + async _onApiSetAllSettings({ value, source }) { this._optionsUtil.validate(value); this._options = clone(value); await this._saveOptions(source); } /** @type {import('api').Handler} */ - async _onApiGetOrCreateSearchPopup({focus=false, text}) { - const {tab, created} = await this._getOrCreateSearchPopup(); + async _onApiGetOrCreateSearchPopup({ focus = false, text }) { + const { tab, created } = await this._getOrCreateSearchPopup(); if (focus === true || (focus === 'ifCreated' && created)) { await this._focusTab(tab); } if (typeof text === 'string') { - const {id} = tab; + const { id } = tab; if (typeof id === 'number') { await this._updateSearchQuery(id, text, !created); } } - const {id} = tab; - return {tabId: typeof id === 'number' ? id : null, windowId: tab.windowId}; + const { id } = tab; + return { tabId: typeof id === 'number' ? id : null, windowId: tab.windowId }; } /** @type {import('api').Handler} */ - async _onApiIsTabSearchPopup({tabId}) { + async _onApiIsTabSearchPopup({ tabId }) { const baseUrl = chrome.runtime.getURL('/search.html'); const tab = typeof tabId === 'number' ? await this._checkTabUrl(tabId, (url) => url !== null && url.startsWith(baseUrl)) : null; return (tab !== null); } /** @type {import('api').Handler} */ - _onApiTriggerDatabaseUpdated({type, cause}) { + _onApiTriggerDatabaseUpdated({ type, cause }) { this._triggerDatabaseUpdated(type, cause); } @@ -840,7 +840,7 @@ export class Backend { let permissionsOkay = false; try { - permissionsOkay = await this._permissionsUtil.hasPermissions({permissions: ['nativeMessaging']}); + permissionsOkay = await this._permissionsUtil.hasPermissions({ permissions: ['nativeMessaging'] }); } catch (e) { // NOP } @@ -870,33 +870,33 @@ export class Backend { } /** @type {import('api').Handler} */ - _onApiTextHasJapaneseCharacters({text}) { + _onApiTextHasJapaneseCharacters({ text }) { return this._japaneseUtil.isStringPartiallyJapanese(text); } /** @type {import('api').Handler} */ - async _onApiGetTermFrequencies({termReadingList, dictionaries}) { + async _onApiGetTermFrequencies({ termReadingList, dictionaries }) { return await this._translator.getTermFrequencies(termReadingList, dictionaries); } /** @type {import('api').Handler} */ - async _onApiFindAnkiNotes({query}) { + async _onApiFindAnkiNotes({ query }) { return await this._anki.findNotes(query); } /** @type {import('api').Handler} */ - async _onApiLoadExtensionScripts({files}, sender) { + async _onApiLoadExtensionScripts({ files }, sender) { if (!sender || !sender.tab) { throw new Error('Invalid sender'); } const tabId = sender.tab.id; if (typeof tabId !== 'number') { throw new Error('Sender has invalid tab ID'); } - const {frameId} = sender; + const { frameId } = sender; for (const file of files) { await this._scriptManager.injectScript(file, tabId, frameId, false); } } /** @type {import('api').Handler} */ - _onApiOpenCrossFramePort({targetTabId, targetFrameId}, sender) { + _onApiOpenCrossFramePort({ targetTabId, targetFrameId }, sender) { const sourceTabId = (sender && sender.tab ? sender.tab.id : null); if (typeof sourceTabId !== 'number') { throw new Error('Port does not have an associated tab ID'); @@ -917,9 +917,9 @@ export class Backend { otherFrameId: sourceFrameId }; /** @type {?chrome.runtime.Port} */ - let sourcePort = chrome.tabs.connect(sourceTabId, {frameId: sourceFrameId, name: JSON.stringify(sourceDetails)}); + 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)}); + let targetPort = chrome.tabs.connect(targetTabId, { frameId: targetFrameId, name: JSON.stringify(targetDetails) }); const cleanup = () => { this._checkLastError(chrome.runtime.lastError); @@ -942,7 +942,7 @@ export class Backend { sourcePort.onDisconnect.addListener(cleanup); targetPort.onDisconnect.addListener(cleanup); - return {targetTabId, targetFrameId}; + return { targetTabId, targetFrameId }; } // Command handlers @@ -971,7 +971,7 @@ export class Backend { } /** @type {import('backend').FindTabsPredicate} */ - const predicate = ({url: url2}) => { + const predicate = ({ url: url2 }) => { if (url2 === null || !url2.startsWith(baseUrl)) { return false; } const parsedUrl = new URL(url2); const baseUrl2 = `${parsedUrl.origin}${parsedUrl.pathname}`; @@ -982,8 +982,8 @@ export class Backend { const openInTab = async () => { const tabInfo = /** @type {?import('backend').TabInfo} */ (await this._findTabs(1000, false, predicate, false)); if (tabInfo !== null) { - const {tab} = tabInfo; - const {id} = tab; + const { tab } = tabInfo; + const { id } = tab; if (typeof id === 'number') { await this._focusTab(tab); if (queryParams.query) { @@ -1033,14 +1033,14 @@ export class Backend { * @returns {Promise} */ async _onCommandToggleTextScanning() { - const options = this._getProfileOptions({current: true}, false); + const options = this._getProfileOptions({ current: true }, false); /** @type {import('settings-modifications').ScopedModificationSet} */ const modification = { action: 'set', path: 'general.enable', value: !options.general.enable, scope: 'profile', - optionsContext: {current: true} + optionsContext: { current: true } }; await this._modifySettings([modification], 'backend'); } @@ -1049,7 +1049,7 @@ export class Backend { * @returns {Promise} */ async _onCommandOpenPopupWindow() { - await this._onApiGetOrCreateSearchPopup({focus: true}); + await this._onApiGetOrCreateSearchPopup({ focus: true }); } // Utilities @@ -1065,9 +1065,9 @@ export class Backend { for (const target of targets) { try { const result = this._modifySetting(target); - results.push({result: clone(result)}); + results.push({ result: clone(result) }); } catch (e) { - results.push({error: ExtensionError.serialize(e)}); + results.push({ error: ExtensionError.serialize(e) }); } } await this._saveOptions(source); @@ -1100,7 +1100,7 @@ export class Backend { if (this._searchPopupTabId !== null) { const tab = await this._checkTabUrl(this._searchPopupTabId, urlPredicate); if (tab !== null) { - return {tab, created: false}; + return { tab, created: false }; } this._searchPopupTabId = null; } @@ -1109,10 +1109,10 @@ export class Backend { const existingTabInfo = await this._findSearchPopupTab(urlPredicate); if (existingTabInfo !== null) { const existingTab = existingTabInfo.tab; - const {id} = existingTab; + const { id } = existingTab; if (typeof id === 'number') { this._searchPopupTabId = id; - return {tab: existingTab, created: false}; + return { tab: existingTab, created: false }; } } @@ -1122,21 +1122,21 @@ export class Backend { } // Create a new window - const options = this._getProfileOptions({current: true}, false); + const options = this._getProfileOptions({ current: true }, false); const createData = this._getSearchPopupWindowCreateData(baseUrl, options); - const {popupWindow: {windowState}} = options; + const { popupWindow: { windowState } } = options; const popupWindow = await this._createWindow(createData); if (windowState !== 'normal' && typeof popupWindow.id === 'number') { - await this._updateWindow(popupWindow.id, {state: windowState}); + await this._updateWindow(popupWindow.id, { state: windowState }); } - const {tabs} = popupWindow; + const { tabs } = popupWindow; if (!Array.isArray(tabs) || tabs.length === 0) { throw new Error('Created window did not contain a tab'); } const tab = tabs[0]; - const {id} = tab; + const { id } = tab; if (typeof id !== 'number') { throw new Error('Tab does not have an id'); } @@ -1144,12 +1144,12 @@ export class Backend { await this._sendMessageTabPromise( id, - {action: 'SearchDisplayController.setMode', params: {mode: 'popup'}}, - {frameId: 0} + { action: 'SearchDisplayController.setMode', params: { mode: 'popup' } }, + { frameId: 0 } ); this._searchPopupTabId = id; - return {tab, created: true}; + return { tab, created: true }; } /** @@ -1158,14 +1158,14 @@ export class Backend { */ async _findSearchPopupTab(urlPredicate) { /** @type {import('backend').FindTabsPredicate} */ - const predicate = async ({url, tab}) => { - const {id} = tab; + const predicate = async ({ url, tab }) => { + const { id } = tab; if (typeof id === 'undefined' || !urlPredicate(url)) { return false; } try { const mode = await this._sendMessageTabPromise( id, - {action: 'SearchDisplayController.getMode', params: {}}, - {frameId: 0} + { action: 'SearchDisplayController.getMode', params: {} }, + { frameId: 0 } ); return mode === 'popup'; } catch (e) { @@ -1181,7 +1181,7 @@ export class Backend { * @returns {chrome.windows.CreateData} */ _getSearchPopupWindowCreateData(url, options) { - const {popupWindow: {width, height, left, top, useLeft, useTop, windowType}} = options; + const { popupWindow: { width, height, left, top, useLeft, useTop, windowType } } = options; return { url, width, @@ -1206,7 +1206,7 @@ export class Backend { if (error) { reject(new Error(error.message)); } else { - resolve(/** @type {chrome.windows.Window} */ (result)); + resolve(/** @type {chrome.windows.Window} */(result)); } } ); @@ -1244,8 +1244,8 @@ export class Backend { async _updateSearchQuery(tabId, text, animate) { await this._sendMessageTabPromise( tabId, - {action: 'SearchDisplayController.updateSearchQuery', params: {text, animate}}, - {frameId: 0} + { action: 'SearchDisplayController.updateSearchQuery', params: { text, animate } }, + { frameId: 0 } ); } @@ -1253,7 +1253,7 @@ export class Backend { * @param {string} source */ _applyOptions(source) { - const options = this._getProfileOptions({current: true}, false); + const options = this._getProfileOptions({ current: true }, false); this._updateBadge(); const enabled = options.general.enable; @@ -1275,7 +1275,7 @@ export class Backend { this._accessibilityController.update(this._getOptionsFull(false)); - this._sendMessageAllTabsIgnoreResponse('Yomitan.optionsUpdated', {source}); + this._sendMessageAllTabsIgnoreResponse('Yomitan.optionsUpdated', { source }); } /** @@ -1309,7 +1309,7 @@ export class Backend { const profiles = options.profiles; if (!optionsContext.current) { // Specific index - const {index} = optionsContext; + const { index } = optionsContext; if (typeof index === 'number') { if (index < 0 || index >= profiles.length) { throw this._createDataError(`Invalid profile index: ${index}`, optionsContext); @@ -1323,7 +1323,7 @@ export class Backend { } } // Default - const {profileCurrent} = options; + const { profileCurrent } = options; if (profileCurrent < 0 || profileCurrent >= profiles.length) { throw this._createDataError(`Invalid current profile index: ${profileCurrent}`, optionsContext); } @@ -1404,50 +1404,52 @@ export class Backend { * @returns {Promise} */ async _textParseScanning(text, scanLength, optionsContext) { - const jp = this._japaneseUtil; - /** @type {import('translator').FindTermsMode} */ - const mode = 'simple'; - const options = this._getProfileOptions(optionsContext, false); - const details = {matchType: /** @type {import('translation').FindTermsMatchType} */ ('exact'), deinflect: true}; - const findTermsOptions = this._getTranslatorFindTermsOptions(mode, details, options); - /** @type {import('api').ParseTextLine[]} */ - const results = []; - let previousUngroupedSegment = null; - let i = 0; - const ii = text.length; - while (i < ii) { - const {dictionaryEntries, originalTextLength} = await this._translator.findTerms( - mode, - text.substring(i, i + scanLength), - findTermsOptions - ); - const codePoint = /** @type {number} */ (text.codePointAt(i)); - const character = String.fromCodePoint(codePoint); - if ( - dictionaryEntries.length > 0 && - originalTextLength > 0 && - (originalTextLength !== character.length || jp.isCodePointJapanese(codePoint)) - ) { - previousUngroupedSegment = null; - const {headwords: [{term, reading}]} = dictionaryEntries[0]; - const source = text.substring(i, i + originalTextLength); - const textSegments = []; - for (const {text: text2, reading: reading2} of jp.distributeFuriganaInflected(term, reading, source)) { - textSegments.push({text: text2, reading: reading2}); - } - results.push(textSegments); - i += originalTextLength; - } else { - if (previousUngroupedSegment === null) { - previousUngroupedSegment = {text: character, reading: ''}; - results.push([previousUngroupedSegment]); - } else { - previousUngroupedSegment.text += character; - } - i += character.length; - } - } - return results; + // const jp = this._japaneseUtil; + // /** @type {import('translator').FindTermsMode} */ + // const mode = 'simple'; + // const options = this._getProfileOptions(optionsContext, false); + // const details = {matchType: /** @type {import('translation').FindTermsMatchType} */ ('exact'), deinflect: true}; + // const findTermsOptions = this._getTranslatorFindTermsOptions(mode, details, options); + // /** @type {import('api').ParseTextLine[]} */ + // const results = []; + // let previousUngroupedSegment = null; + // let i = 0; + // const ii = text.length; + // while (i < ii) { + // const {dictionaryEntries, originalTextLength} = await this._translator.findTerms( + // mode, + // text.substring(i, i + scanLength), + // findTermsOptions + // ); + // const codePoint = /** @type {number} */ (text.codePointAt(i)); + // const character = String.fromCodePoint(codePoint); + // if ( + // dictionaryEntries.length > 0 && + // originalTextLength > 0 && + // (originalTextLength !== character.length || jp.isCodePointJapanese(codePoint)) + // ) { + // previousUngroupedSegment = null; + // const {headwords: [{term, reading}]} = dictionaryEntries[0]; + // const source = text.substring(i, i + originalTextLength); + // const textSegments = []; + // for (const {text: text2, reading: reading2} of jp.distributeFuriganaInflected(term, reading, source)) { + // textSegments.push({text: text2, reading: reading2}); + // } + // results.push(textSegments); + // i += originalTextLength; + // } else { + // if (previousUngroupedSegment === null) { + // previousUngroupedSegment = {text: character, reading: ''}; + // results.push([previousUngroupedSegment]); + // } else { + // previousUngroupedSegment.text += character; + // } + // i += character.length; + // } + // } + // return results; + + return [[{ "text": text, "reading": "" }]]; } /** @@ -1466,22 +1468,22 @@ export class Backend { /** @type {import('backend').MecabParseResults} */ const results = []; - for (const {name, lines} of parseTextResults) { + for (const { name, lines } of parseTextResults) { /** @type {import('api').ParseTextLine[]} */ const result = []; for (const line of lines) { - for (const {term, reading, source} of line) { + for (const { term, reading, source } of line) { const termParts = []; - for (const {text: text2, reading: reading2} of jp.distributeFuriganaInflected( + for (const { text: text2, reading: reading2 } of jp.distributeFuriganaInflected( term.length > 0 ? term : source, jp.convertKatakanaToHiragana(reading), source )) { - termParts.push({text: text2, reading: reading2}); + termParts.push({ text: text2, reading: reading2 }); } result.push(termParts); } - result.push([{text: '\n', reading: ''}]); + result.push([{ text: '\n', reading: '' }]); } results.push([name, result]); } @@ -1505,7 +1507,7 @@ export class Backend { const onProgress = (...data) => { try { if (done) { return; } - port.postMessage(/** @type {import('backend').InvokeWithProgressResponseProgressMessage} */ ({type: 'progress', data})); + port.postMessage(/** @type {import('backend').InvokeWithProgressResponseProgressMessage} */({ type: 'progress', data })); } catch (e) { // NOP } @@ -1518,7 +1520,7 @@ export class Backend { if (hasStarted) { return; } try { - const {action} = message; + const { action } = message; switch (action) { case 'fragment': messageString += message.data; @@ -1544,14 +1546,14 @@ export class Backend { */ const onMessageComplete = async (message) => { try { - const {action, params} = message; - port.postMessage(/** @type {import('backend').InvokeWithProgressResponseAcknowledgeMessage} */ ({type: 'ack'})); + const { action, params } = message; + port.postMessage(/** @type {import('backend').InvokeWithProgressResponseAcknowledgeMessage} */({ type: 'ack' })); const messageHandler = handlers.get(action); if (typeof messageHandler === 'undefined') { throw new Error('Invalid action'); } - const {handler, async, contentScript} = messageHandler; + const { handler, async, contentScript } = messageHandler; if (!contentScript) { this._validatePrivilegedMessageSender(sender); @@ -1559,7 +1561,7 @@ export class Backend { const promiseOrResult = handler(params, sender, onProgress); const result = async ? await promiseOrResult : promiseOrResult; - port.postMessage(/** @type {import('backend').InvokeWithProgressResponseCompleteMessage} */ ({type: 'complete', data: result})); + port.postMessage(/** @type {import('backend').InvokeWithProgressResponseCompleteMessage} */({ type: 'complete', data: result })); } catch (e) { cleanup(e); } @@ -1575,7 +1577,7 @@ export class Backend { const cleanup = (error) => { if (done) { return; } if (error !== null) { - port.postMessage(/** @type {import('backend').InvokeWithProgressResponseErrorMessage} */ ({type: 'error', data: ExtensionError.serialize(error)})); + port.postMessage(/** @type {import('backend').InvokeWithProgressResponseErrorMessage} */({ type: 'error', data: ExtensionError.serialize(error) })); } if (!hasStarted) { port.onMessage.removeListener(onMessage); @@ -1611,11 +1613,11 @@ export class Backend { const scope = target.scope; switch (scope) { case 'profile': - { - const {optionsContext} = target; - if (typeof optionsContext !== 'object' || optionsContext === null) { throw new Error('Invalid optionsContext'); } - return /** @type {import('settings').ProfileOptions} */ (this._getProfileOptions(optionsContext, true)); - } + { + const { optionsContext } = target; + if (typeof optionsContext !== 'object' || optionsContext === null) { throw new Error('Invalid optionsContext'); } + return /** @type {import('settings').ProfileOptions} */ (this._getProfileOptions(optionsContext, true)); + } case 'global': return /** @type {import('settings').Options} */ (this._getOptionsFull(true)); default: @@ -1631,7 +1633,7 @@ export class Backend { _getSetting(target) { const options = this._getModifySettingObject(target); const accessor = new ObjectPropertyAccessor(options); - const {path} = target; + const { path } = target; if (typeof path !== 'string') { throw new Error('Invalid path'); } return accessor.get(ObjectPropertyAccessor.getPathArray(path)); } @@ -1647,50 +1649,50 @@ export class Backend { const action = target.action; switch (action) { case 'set': - { - const {path, value} = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - const pathArray = ObjectPropertyAccessor.getPathArray(path); - accessor.set(pathArray, value); - return accessor.get(pathArray); - } + { + const { path, value } = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + const pathArray = ObjectPropertyAccessor.getPathArray(path); + accessor.set(pathArray, value); + return accessor.get(pathArray); + } case 'delete': - { - const {path} = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - accessor.delete(ObjectPropertyAccessor.getPathArray(path)); - return true; - } + { + const { path } = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + accessor.delete(ObjectPropertyAccessor.getPathArray(path)); + return true; + } case 'swap': - { - const {path1, path2} = target; - if (typeof path1 !== 'string') { throw new Error('Invalid path1'); } - if (typeof path2 !== 'string') { throw new Error('Invalid path2'); } - accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2)); - return true; - } + { + const { path1, path2 } = target; + if (typeof path1 !== 'string') { throw new Error('Invalid path1'); } + if (typeof path2 !== 'string') { throw new Error('Invalid path2'); } + accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2)); + return true; + } case 'splice': - { - const {path, start, deleteCount, items} = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); } - if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); } - if (!Array.isArray(items)) { throw new Error('Invalid items'); } - const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); - if (!Array.isArray(array)) { throw new Error('Invalid target type'); } - return array.splice(start, deleteCount, ...items); - } + { + const { path, start, deleteCount, items } = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); } + if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); } + if (!Array.isArray(items)) { throw new Error('Invalid items'); } + const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); + if (!Array.isArray(array)) { throw new Error('Invalid target type'); } + return array.splice(start, deleteCount, ...items); + } case 'push': - { - const {path, items} = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - if (!Array.isArray(items)) { throw new Error('Invalid items'); } - const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); - if (!Array.isArray(array)) { throw new Error('Invalid target type'); } - const start = array.length; - array.push(...items); - return start; - } + { + const { path, items } = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + if (!Array.isArray(items)) { throw new Error('Invalid items'); } + const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); + if (!Array.isArray(array)) { throw new Error('Invalid target type'); } + const start = array.length; + array.push(...items); + return start; + } default: throw new Error(`Unknown action: ${action}`); } @@ -1701,11 +1703,11 @@ export class Backend { * @throws {Error} */ _validatePrivilegedMessageSender(sender) { - let {url} = sender; + let { url } = sender; if (typeof url === 'string' && yomitan.isExtensionUrl(url)) { return; } - const {tab} = sender; + const { tab } = sender; if (typeof tab === 'object' && tab !== null) { - ({url} = tab); + ({ url } = tab); if (typeof url === 'string' && yomitan.isExtensionUrl(url)) { return; } } throw new Error('Invalid message sender'); @@ -1717,7 +1719,7 @@ export class Backend { _getBrowserIconTitle() { return ( isObject(chrome.action) && - typeof chrome.action.getTitle === 'function' ? + typeof chrome.action.getTitle === 'function' ? new Promise((resolve) => chrome.action.getTitle({}, resolve)) : Promise.resolve('') ); @@ -1761,7 +1763,7 @@ export class Backend { status = 'Loading'; } } else { - const options = this._getProfileOptions({current: true}, false); + const options = this._getProfileOptions({ current: true }, false); if (!options.general.enable) { text = 'off'; color = '#555555'; @@ -1778,16 +1780,16 @@ export class Backend { } if (color !== null && typeof chrome.action.setBadgeBackgroundColor === 'function') { - chrome.action.setBadgeBackgroundColor({color}); + chrome.action.setBadgeBackgroundColor({ color }); } if (text !== null && typeof chrome.action.setBadgeText === 'function') { - chrome.action.setBadgeText({text}); + chrome.action.setBadgeText({ text }); } if (typeof chrome.action.setTitle === 'function') { if (status !== null) { title = `${title} - ${status}`; } - chrome.action.setTitle({title}); + chrome.action.setTitle({ title }); } } @@ -1796,7 +1798,7 @@ export class Backend { * @returns {boolean} */ _isAnyDictionaryEnabled(options) { - for (const {enabled} of options.dictionaries) { + for (const { enabled } of options.dictionaries) { if (enabled) { return true; } @@ -1812,8 +1814,8 @@ export class Backend { try { const response = await this._sendMessageTabPromise( tabId, - {action: 'Yomitan.getUrl', params: {}}, - {frameId: 0} + { action: 'Yomitan.getUrl', params: {} }, + { frameId: 0 } ); const url = typeof response === 'object' && response !== null ? /** @type {import('core').SerializableObject} */ (response).url : void 0; if (typeof url === 'string') { @@ -1858,13 +1860,13 @@ export class Backend { * @param {(tabInfo: import('backend').TabInfo) => boolean} add */ const checkTab = async (tab, add) => { - const {id} = tab; + const { id } = tab; const url = typeof id === 'number' ? await this._getTabUrl(id) : null; if (done) { return; } let okay = false; - const item = {tab, url}; + const item = { tab, url }; try { const okayOrPromise = predicate(item); okay = predicateIsAsync ? await okayOrPromise : /** @type {boolean} */ (okayOrPromise); @@ -1897,7 +1899,7 @@ export class Backend { ]); return results; } else { - const {promise, resolve} = /** @type {import('core').DeferredPromiseDetails} */ (deferPromise()); + const { promise, resolve } = /** @type {import('core').DeferredPromiseDetails} */ (deferPromise()); /** @type {?import('backend').TabInfo} */ let result = null; /** @@ -1925,12 +1927,12 @@ export class Backend { */ async _focusTab(tab) { await /** @type {Promise} */ (new Promise((resolve, reject) => { - const {id} = tab; + const { id } = tab; if (typeof id !== 'number') { reject(new Error('Cannot focus a tab without an id')); return; } - chrome.tabs.update(id, {active: true}, () => { + chrome.tabs.update(id, { active: true }, () => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); @@ -1958,7 +1960,7 @@ export class Backend { }); if (!tabWindow.focused) { await /** @type {Promise} */ (new Promise((resolve, reject) => { - chrome.windows.update(tab.windowId, {focused: true}, () => { + chrome.windows.update(tab.windowId, { focused: true }, () => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); @@ -1979,7 +1981,7 @@ export class Backend { * @param {?number} [timeout=null] * @returns {Promise} */ - _waitUntilTabFrameIsReady(tabId, frameId, timeout=null) { + _waitUntilTabFrameIsReady(tabId, frameId, timeout = null) { return new Promise((resolve, reject) => { /** @type {?import('core').Timeout} */ let timer = null; @@ -2011,14 +2013,14 @@ export class Backend { chrome.runtime.onMessage.addListener(onMessage); - this._sendMessageTabPromise(tabId, {action: 'Yomitan.isReady'}, {frameId}) + this._sendMessageTabPromise(tabId, { action: 'Yomitan.isReady' }, { frameId }) .then( (value) => { if (!value) { return; } cleanup(); resolve(); }, - () => {} // NOP + () => { } // NOP ); if (timeout !== null) { @@ -2094,9 +2096,9 @@ export class Backend { const callback = () => this._checkLastError(chrome.runtime.lastError); chrome.tabs.query({}, (tabs) => { for (const tab of tabs) { - const {id} = tab; + const { id } = tab; if (typeof id !== 'number') { continue; } - chrome.tabs.sendMessage(id, {action, params}, callback); + chrome.tabs.sendMessage(id, { action, params }, callback); } }); } @@ -2171,18 +2173,18 @@ export class Backend { */ async _getScreenshot(tabId, frameId, format, quality) { const tab = await this._getTabById(tabId); - const {windowId} = tab; + const { windowId } = tab; let token = null; try { if (typeof tabId === 'number' && typeof frameId === 'number') { const action = 'Frontend.setAllVisibleOverride'; - const params = {value: false, priority: 0, awaitFrame: true}; - token = await this._sendMessageTabPromise(tabId, {action, params}, {frameId}); + const params = { value: false, priority: 0, awaitFrame: true }; + token = await this._sendMessageTabPromise(tabId, { action, params }, { frameId }); } return await new Promise((resolve, reject) => { - chrome.tabs.captureVisibleTab(windowId, {format, quality}, (result) => { + chrome.tabs.captureVisibleTab(windowId, { format, quality }, (result) => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); @@ -2194,9 +2196,9 @@ export class Backend { } finally { if (token !== null) { const action = 'Frontend.clearAllVisibleOverride'; - const params = {token}; + const params = { token }; try { - await this._sendMessageTabPromise(tabId, {action, params}, {frameId}); + await this._sendMessageTabPromise(tabId, { action, params }, { frameId }); } catch (e) { // NOP } @@ -2257,7 +2259,7 @@ export class Backend { let dictionaryMedia; try { let errors2; - ({results: dictionaryMedia, errors: errors2} = await this._injectAnkiNoteDictionaryMedia(ankiConnect, timestamp, definitionDetails, dictionaryMediaDetails)); + ({ results: dictionaryMedia, errors: errors2 } = await this._injectAnkiNoteDictionaryMedia(ankiConnect, timestamp, definitionDetails, dictionaryMediaDetails)); for (const error of errors2) { errors.push(ExtensionError.serialize(error)); } @@ -2285,14 +2287,14 @@ export class Backend { */ async _injectAnkiNoteAudio(ankiConnect, timestamp, definitionDetails, details) { if (definitionDetails.type !== 'term') { return null; } - const {term, reading} = definitionDetails; + const { term, reading } = definitionDetails; if (term.length === 0 && reading.length === 0) { return null; } - const {sources, preferredAudioIndex, idleTimeout} = details; + const { sources, preferredAudioIndex, idleTimeout } = details; let data; let contentType; try { - ({data, contentType} = await this._audioDownloader.downloadTermAudio( + ({ data, contentType } = await this._audioDownloader.downloadTermAudio( sources, preferredAudioIndex, term, @@ -2321,10 +2323,10 @@ export class Backend { * @returns {Promise} */ async _injectAnkiNoteScreenshot(ankiConnect, timestamp, definitionDetails, details) { - const {tabId, frameId, format, quality} = details; + const { tabId, frameId, format, quality } = details; const dataUrl = await this._getScreenshot(tabId, frameId, format, quality); - const {mediaType, data} = this._getDataUrlInfo(dataUrl); + const { mediaType, data } = this._getDataUrlInfo(dataUrl); const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); if (extension === null) { throw new Error('Unknown media type for screenshot image'); @@ -2346,7 +2348,7 @@ export class Backend { return null; } - const {mediaType, data} = this._getDataUrlInfo(dataUrl); + const { mediaType, data } = this._getDataUrlInfo(dataUrl); const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); if (extension === null) { throw new Error('Unknown media type for clipboard image'); @@ -2367,9 +2369,9 @@ export class Backend { const targets = []; const detailsList = []; const detailsMap = new Map(); - for (const {dictionary, path} of dictionaryMediaDetails) { - const target = {dictionary, path}; - const details = {dictionary, path, media: null}; + for (const { dictionary, path } of dictionaryMediaDetails) { + const target = { dictionary, path }; + const details = { dictionary, path, media: null }; const key = JSON.stringify(target); targets.push(target); detailsList.push(details); @@ -2378,8 +2380,8 @@ export class Backend { const mediaList = await this._getNormalizedDictionaryDatabaseMedia(targets); for (const media of mediaList) { - const {dictionary, path} = media; - const key = JSON.stringify({dictionary, path}); + const { dictionary, path } = media; + const key = JSON.stringify({ dictionary, path }); const details = detailsMap.get(key); if (typeof details === 'undefined' || details.media !== null) { continue; } details.media = media; @@ -2389,10 +2391,10 @@ export class Backend { /** @type {import('api').InjectAnkiNoteDictionaryMediaResult[]} */ const results = []; for (let i = 0, ii = detailsList.length; i < ii; ++i) { - const {dictionary, path, media} = detailsList[i]; + const { dictionary, path, media } = detailsList[i]; let fileName = null; if (media !== null) { - const {content, mediaType} = media; + const { content, mediaType } = media; const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); fileName = this._generateAnkiNoteMediaFileName( `yomitan_dictionary_media_${i + 1}`, @@ -2407,10 +2409,10 @@ export class Backend { fileName = null; } } - results.push({dictionary, path, fileName}); + results.push({ dictionary, path, fileName }); } - return {results, errors}; + return { results, errors }; } /** @@ -2419,7 +2421,7 @@ export class Backend { */ _getAudioDownloadError(error) { if (error instanceof ExtensionError && typeof error.data === 'object' && error.data !== null) { - const {errors} = /** @type {import('core').SerializableObject} */ (error.data); + const { errors } = /** @type {import('core').SerializableObject} */ (error.data); if (Array.isArray(errors)) { for (const error2 of errors) { if (!(error2 instanceof Error)) { continue; } @@ -2427,9 +2429,9 @@ export class Backend { return this._createAudioDownloadError('Audio download was cancelled due to an idle timeout', 'audio-download-idle-timeout', errors); } if (!(error2 instanceof ExtensionError)) { continue; } - const {data} = error2; + const { data } = error2; if (!(typeof data === 'object' && data !== null)) { continue; } - const {details} = /** @type {import('core').SerializableObject} */ (data); + const { details } = /** @type {import('core').SerializableObject} */ (data); if (!(typeof details === 'object' && details !== null)) { continue; } const error3 = /** @type {import('core').SerializableObject} */ (details).error; if (typeof error3 !== 'string') { continue; } @@ -2488,13 +2490,13 @@ export class Backend { switch (definitionDetails.type) { case 'kanji': { - const {character} = definitionDetails; + const { character } = definitionDetails; if (character) { fileName += `_${character}`; } } break; default: { - const {reading, term} = definitionDetails; + const { reading, term } = definitionDetails; if (reading) { fileName += `_${reading}`; } if (term) { fileName += `_${term}`; } } @@ -2549,7 +2551,7 @@ export class Backend { let data = dataUrl.substring(match[0].length); if (typeof match[2] === 'undefined') { data = btoa(data); } - return {mediaType, data}; + return { mediaType, data }; } /** @@ -2558,7 +2560,7 @@ export class Backend { */ _triggerDatabaseUpdated(type, cause) { this._translator.clearDatabaseCaches(); - this._sendMessageAllTabsIgnoreResponse('Yomitan.databaseUpdated', {type, cause}); + this._sendMessageAllTabsIgnoreResponse('Yomitan.databaseUpdated', { type, cause }); } /** @@ -2579,13 +2581,13 @@ export class Backend { * @returns {import('translation').FindTermsOptions} An options object. */ _getTranslatorFindTermsOptions(mode, details, options) { - let {matchType, deinflect} = details; + let { matchType, deinflect } = details; if (typeof matchType !== 'string') { matchType = /** @type {import('translation').FindTermsMatchType} */ ('exact'); } if (typeof deinflect !== 'boolean') { deinflect = true; } const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options); const { - general: {mainDictionary, sortFrequencyDictionary, sortFrequencyDictionaryOrder}, - scanning: {alphanumeric}, + general: { mainDictionary, sortFrequencyDictionary, sortFrequencyDictionaryOrder }, + scanning: { alphanumeric }, translation: { convertHalfWidthCharacters, convertNumericCharacters, @@ -2633,7 +2635,7 @@ export class Backend { */ _getTranslatorFindKanjiOptions(options) { const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options); - return {enabledDictionaryMap}; + return { enabledDictionaryMap }; } /** @@ -2663,7 +2665,7 @@ export class Backend { for (const group of textReplacementsOptions.groups) { /** @type {import('translation').FindTermsTextReplacement[]} */ const textReplacementsEntries = []; - for (const {pattern, ignoreCase, replacement} of group) { + for (const { pattern, ignoreCase, replacement } of group) { let patternRegExp; try { patternRegExp = new RegExp(pattern, ignoreCase ? 'gi' : 'g'); @@ -2671,7 +2673,7 @@ export class Backend { // Invalid pattern continue; } - textReplacementsEntries.push({pattern: patternRegExp, replacement}); + textReplacementsEntries.push({ pattern: patternRegExp, replacement }); } if (textReplacementsEntries.length > 0) { textReplacements.push(textReplacementsEntries); @@ -2690,7 +2692,7 @@ export class Backend { chrome.storage.session.get(['openedWelcomePage']).then((result) => { if (!result.openedWelcomePage) { this._openWelcomeGuidePage(); - chrome.storage.session.set({'openedWelcomePage': true}); + chrome.storage.session.set({ 'openedWelcomePage': true }); } }); } @@ -2716,7 +2718,7 @@ export class Backend { const manifest = chrome.runtime.getManifest(); const optionsUI = manifest.options_ui; if (typeof optionsUI === 'undefined') { throw new Error('Failed to find options_ui'); } - const {page} = optionsUI; + const { page } = optionsUI; if (typeof page === 'undefined') { throw new Error('Failed to find options_ui.page'); } const url = chrome.runtime.getURL(page); switch (mode) { @@ -2744,7 +2746,7 @@ export class Backend { */ _createTab(url) { return new Promise((resolve, reject) => { - chrome.tabs.create({url}, (tab) => { + chrome.tabs.create({ url }, (tab) => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); @@ -2808,7 +2810,7 @@ export class Backend { // Only request this permission for Firefox versions >= 77. // https://bugzilla.mozilla.org/show_bug.cgi?id=1630413 - const {vendor, version} = await browser.runtime.getBrowserInfo(); + const { vendor, version } = await browser.runtime.getBrowserInfo(); if (vendor !== 'Mozilla') { return; } const match = /^\d+/.exec(version); @@ -2830,9 +2832,9 @@ export class Backend { async _getNormalizedDictionaryDatabaseMedia(targets) { const results = []; for (const item of await this._dictionaryDatabase.getMedia(targets)) { - const {content, dictionary, height, mediaType, path, width} = item; + const { content, dictionary, height, mediaType, path, width } = item; const content2 = ArrayBufferUtil.arrayBufferToBase64(content); - results.push({content: content2, dictionary, height, mediaType, path, width}); + results.push({ content: content2, dictionary, height, mediaType, path, width }); } return results; } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 6e1450c3a1..740cff10a4 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2017-2022 Yomichan Authors * @@ -16,24 +17,24 @@ * along with this program. If not, see . */ -import {Frontend} from '../app/frontend.js'; -import {PopupFactory} from '../app/popup-factory.js'; -import {ThemeController} from '../app/theme-controller.js'; -import {FrameEndpoint} from '../comm/frame-endpoint.js'; -import {DynamicProperty, EventDispatcher, EventListenerCollection, clone, deepEqual, invokeMessageHandler, log, promiseTimeout} from '../core.js'; -import {PopupMenu} from '../dom/popup-menu.js'; -import {ScrollElement} from '../dom/scroll-element.js'; -import {HotkeyHelpController} from '../input/hotkey-help-controller.js'; -import {TextScanner} from '../language/text-scanner.js'; -import {dynamicLoader} from '../script/dynamic-loader.js'; -import {yomitan} from '../yomitan.js'; -import {DisplayContentManager} from './display-content-manager.js'; -import {DisplayGenerator} from './display-generator.js'; -import {DisplayHistory} from './display-history.js'; -import {DisplayNotification} from './display-notification.js'; -import {ElementOverflowController} from './element-overflow-controller.js'; -import {OptionToggleHotkeyHandler} from './option-toggle-hotkey-handler.js'; -import {QueryParser} from './query-parser.js'; +import { Frontend } from '../app/frontend.js'; +import { PopupFactory } from '../app/popup-factory.js'; +import { ThemeController } from '../app/theme-controller.js'; +import { FrameEndpoint } from '../comm/frame-endpoint.js'; +import { DynamicProperty, EventDispatcher, EventListenerCollection, clone, deepEqual, invokeMessageHandler, log, promiseTimeout } from '../core.js'; +import { PopupMenu } from '../dom/popup-menu.js'; +import { ScrollElement } from '../dom/scroll-element.js'; +import { HotkeyHelpController } from '../input/hotkey-help-controller.js'; +import { TextScanner } from '../language/text-scanner.js'; +import { dynamicLoader } from '../script/dynamic-loader.js'; +import { yomitan } from '../yomitan.js'; +import { DisplayContentManager } from './display-content-manager.js'; +import { DisplayGenerator } from './display-generator.js'; +import { DisplayHistory } from './display-history.js'; +import { DisplayNotification } from './display-notification.js'; +import { ElementOverflowController } from './element-overflow-controller.js'; +import { OptionToggleHotkeyHandler } from './option-toggle-hotkey-handler.js'; +import { QueryParser } from './query-parser.js'; /** * @augments EventDispatcher @@ -68,7 +69,7 @@ export class Display extends EventDispatcher { /** @type {HTMLElement[]} */ this._dictionaryEntryNodes = []; /** @type {import('settings').OptionsContext} */ - this._optionsContext = {depth: 0, url: window.location.href}; + this._optionsContext = { depth: 0, url: window.location.href }; /** @type {?import('settings').ProfileOptions} */ this._options = null; /** @type {number} */ @@ -96,7 +97,7 @@ export class Display extends EventDispatcher { /** @type {import('core').MessageHandlerMap} */ this._windowMessageHandlers = new Map(); /** @type {DisplayHistory} */ - this._history = new DisplayHistory({clearable: true, useBrowserHistory: false}); + this._history = new DisplayHistory({ clearable: true, useBrowserHistory: false }); /** @type {boolean} */ this._historyChangeIgnore = false; /** @type {boolean} */ @@ -196,27 +197,27 @@ export class Display extends EventDispatcher { this._themeController = new ThemeController(document.documentElement); this._hotkeyHandler.registerActions([ - ['close', () => { this._onHotkeyClose(); }], - ['nextEntry', this._onHotkeyActionMoveRelative.bind(this, 1)], - ['previousEntry', this._onHotkeyActionMoveRelative.bind(this, -1)], - ['lastEntry', () => { this._focusEntry(this._dictionaryEntries.length - 1, 0, true); }], - ['firstEntry', () => { this._focusEntry(0, 0, true); }], - ['historyBackward', () => { this._sourceTermView(); }], - ['historyForward', () => { this._nextTermView(); }], + ['close', () => { this._onHotkeyClose(); }], + ['nextEntry', this._onHotkeyActionMoveRelative.bind(this, 1)], + ['previousEntry', this._onHotkeyActionMoveRelative.bind(this, -1)], + ['lastEntry', () => { this._focusEntry(this._dictionaryEntries.length - 1, 0, true); }], + ['firstEntry', () => { this._focusEntry(0, 0, true); }], + ['historyBackward', () => { this._sourceTermView(); }], + ['historyForward', () => { this._nextTermView(); }], ['copyHostSelection', () => this._copyHostSelection()], - ['nextEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(1, true); }], + ['nextEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(1, true); }], ['previousEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(-1, true); }] ]); this.registerDirectMessageHandlers([ - ['Display.setOptionsContext', {async: true, handler: this._onMessageSetOptionsContext.bind(this)}], - ['Display.setContent', {async: false, handler: this._onMessageSetContent.bind(this)}], - ['Display.setCustomCss', {async: false, handler: this._onMessageSetCustomCss.bind(this)}], - ['Display.setContentScale', {async: false, handler: this._onMessageSetContentScale.bind(this)}], - ['Display.configure', {async: true, handler: this._onMessageConfigure.bind(this)}], - ['Display.visibilityChanged', {async: false, handler: this._onMessageVisibilityChanged.bind(this)}] + ['Display.setOptionsContext', { async: true, handler: this._onMessageSetOptionsContext.bind(this) }], + ['Display.setContent', { async: false, handler: this._onMessageSetContent.bind(this) }], + ['Display.setCustomCss', { async: false, handler: this._onMessageSetCustomCss.bind(this) }], + ['Display.setContentScale', { async: false, handler: this._onMessageSetContentScale.bind(this) }], + ['Display.configure', { async: true, handler: this._onMessageConfigure.bind(this) }], + ['Display.visibilityChanged', { async: false, handler: this._onMessageVisibilityChanged.bind(this) }] ]); this.registerWindowMessageHandlers([ - ['Display.extensionUnloaded', {async: false, handler: this._onMessageExtensionUnloaded.bind(this)}] + ['Display.extensionUnloaded', { async: false, handler: this._onMessageExtensionUnloaded.bind(this) }] ]); } @@ -307,8 +308,8 @@ export class Display extends EventDispatcher { this._themeController.prepare(); // State setup - const {documentElement} = document; - const {browser} = await yomitan.api.getEnvironmentInfo(); + const { documentElement } = document; + const { browser } = await yomitan.api.getEnvironmentInfo(); this._browser = browser; if (documentElement !== null) { @@ -328,7 +329,7 @@ export class Display extends EventDispatcher { this._progressIndicatorVisible.on('change', this._onProgressIndicatorVisibleChanged.bind(this)); yomitan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this)); yomitan.crossFrame.registerHandlers([ - ['popupMessage', {async: 'dynamic', handler: this._onDirectMessage.bind(this)}] + ['popupMessage', { async: 'dynamic', handler: this._onDirectMessage.bind(this) }] ]); window.addEventListener('message', this._onWindowMessage.bind(this), false); @@ -338,7 +339,7 @@ export class Display extends EventDispatcher { documentElement.addEventListener('auxclick', this._onDocumentElementClick.bind(this), false); } - document.addEventListener('wheel', this._onWheel.bind(this), {passive: false}); + document.addEventListener('wheel', this._onWheel.bind(this), { passive: false }); if (this._closeButton !== null) { this._closeButton.addEventListener('click', this._onCloseButtonClick.bind(this), false); } @@ -371,7 +372,7 @@ export class Display extends EventDispatcher { /** * @param {{clearable?: boolean, useBrowserHistory?: boolean}} details */ - setHistorySettings({clearable, useBrowserHistory}) { + setHistorySettings({ clearable, useBrowserHistory }) { if (typeof clearable !== 'undefined') { this._history.clearable = clearable; } @@ -413,7 +414,7 @@ export class Display extends EventDispatcher { /** */ async updateOptions() { const options = await yomitan.api.optionsGet(this.getOptionsContext()); - const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options; + const { scanning: scanningOptions, sentenceParsing: sentenceParsingOptions } = options; this._options = options; this._updateHotkeys(options); @@ -450,7 +451,7 @@ export class Display extends EventDispatcher { this._updateContentTextScanner(options); /** @type {import('display').OptionsUpdatedEvent} */ - const event = {options}; + const event = { options }; this.trigger('optionsUpdated', event); } @@ -459,7 +460,7 @@ export class Display extends EventDispatcher { * @param {import('display').ContentDetails} details Information about the content to show. */ setContent(details) { - const {focus, params, state, content} = details; + const { focus, params, state, content } = details; const historyMode = this._historyHasChanged ? details.historyMode : 'clear'; if (focus) { @@ -549,19 +550,19 @@ export class Display extends EventDispatcher { const type = this._contentType; if (type === 'clear') { return; } const query = this._query; - const {state} = this._history; + const { state } = this._history; const hasState = typeof state === 'object' && state !== null; /** @type {import('display').HistoryState} */ const newState = ( hasState ? - clone(state) : - { - focusEntry: 0, - optionsContext: void 0, - url: window.location.href, - sentence: {text: query, offset: 0}, - documentTitle: document.title - } + clone(state) : + { + focusEntry: 0, + optionsContext: void 0, + url: window.location.href, + sentence: { text: query, offset: 0 }, + documentTitle: document.title + } ); if (!hasState || updateOptionsContext) { newState.optionsContext = clone(this._optionsContext); @@ -585,7 +586,7 @@ export class Display extends EventDispatcher { * @param {import('core').SerializableObject} [params] * @returns {Promise} */ - async invokeContentOrigin(action, params={}) { + async invokeContentOrigin(action, params = {}) { if (this._contentOriginTabId === this._tabId && this._contentOriginFrameId === this._frameId) { throw new Error('Content origin is same page'); } @@ -601,7 +602,7 @@ export class Display extends EventDispatcher { * @param {import('core').SerializableObject} [params] * @returns {Promise} */ - async invokeParentFrame(action, params={}) { + async invokeParentFrame(action, params = {}) { if (this._parentFrameId === null || this._parentFrameId === this._frameId) { throw new Error('Invalid parent frame'); } @@ -615,7 +616,7 @@ export class Display extends EventDispatcher { getElementDictionaryEntryIndex(element) { const node = /** @type {?HTMLElement} */ (element.closest('.entry')); if (node === null) { return -1; } - const {index} = node.dataset; + const { index } = node.dataset; if (typeof index !== 'string') { return -1; } const indexNumber = parseInt(index, 10); return Number.isFinite(indexNumber) ? indexNumber : -1; @@ -642,13 +643,13 @@ export class Display extends EventDispatcher { * @throws {Error} */ _onDirectMessage(data) { - const {action, params} = this._authenticateMessageData(data); + const { action, params } = this._authenticateMessageData(data); const handlerInfo = this._directMessageHandlers.get(action); if (typeof handlerInfo === 'undefined') { throw new Error(`Invalid action: ${action}`); } - const {async, handler} = handlerInfo; + const { async, handler } = handlerInfo; const result = handler(params); return { async: typeof async === 'boolean' && async, @@ -659,7 +660,7 @@ export class Display extends EventDispatcher { /** * @param {MessageEvent>} details */ - _onWindowMessage({data}) { + _onWindowMessage({ data }) { let data2; try { data2 = this._authenticateMessageData(data); @@ -667,18 +668,18 @@ export class Display extends EventDispatcher { return; } - const {action, params} = data2; + const { action, params } = data2; const messageHandler = this._windowMessageHandlers.get(action); if (typeof messageHandler === 'undefined') { return; } - const callback = () => {}; // NOP + const callback = () => { }; // NOP invokeMessageHandler(messageHandler, params, callback); } /** * @param {{optionsContext: import('settings').OptionsContext}} details */ - async _onMessageSetOptionsContext({optionsContext}) { + async _onMessageSetOptionsContext({ optionsContext }) { await this.setOptionsContext(optionsContext); this.searchLast(true); } @@ -686,28 +687,28 @@ export class Display extends EventDispatcher { /** * @param {{details: import('display').ContentDetails}} details */ - _onMessageSetContent({details}) { + _onMessageSetContent({ details }) { this.setContent(details); } /** * @param {{css: string}} details */ - _onMessageSetCustomCss({css}) { + _onMessageSetCustomCss({ css }) { this.setCustomCss(css); } /** * @param {{scale: number}} details */ - _onMessageSetContentScale({scale}) { + _onMessageSetContentScale({ scale }) { this._setContentScale(scale); } /** * @param {import('display').ConfigureMessageDetails} details */ - async _onMessageConfigure({depth, parentPopupId, parentFrameId, childrenSupported, scale, optionsContext}) { + async _onMessageConfigure({ depth, parentPopupId, parentFrameId, childrenSupported, scale, optionsContext }) { this._depth = depth; this._parentPopupId = parentPopupId; this._parentFrameId = parentFrameId; @@ -719,10 +720,10 @@ export class Display extends EventDispatcher { /** * @param {{value: boolean}} details */ - _onMessageVisibilityChanged({value}) { + _onMessageVisibilityChanged({ value }) { this._frameVisible = value; /** @type {import('display').FrameVisibilityChangeEvent} */ - const event = {value}; + const event = { value }; this.trigger('frameVisibilityChange', event); } @@ -803,7 +804,7 @@ export class Display extends EventDispatcher { /** * @param {import('display').QueryParserSearchedEvent} details */ - _onQueryParserSearch({type, dictionaryEntries, sentence, inputInfo: {eventType}, textSource, optionsContext, sentenceOffset}) { + _onQueryParserSearch({ type, dictionaryEntries, sentence, inputInfo: { eventType }, textSource, optionsContext, sentenceOffset }) { const query = textSource.text(); const historyState = this._history.state; const historyMode = ( @@ -837,7 +838,7 @@ export class Display extends EventDispatcher { const details = { focus: false, historyMode: 'clear', - params: {type}, + params: { type }, state: {}, content: { contentOrigin: { @@ -876,7 +877,7 @@ export class Display extends EventDispatcher { /** * @param {import('dynamic-property').ChangeEventDetails} details */ - _onProgressIndicatorVisibleChanged({value}) { + _onProgressIndicatorVisibleChanged({ value }) { if (this._progressIndicatorTimer !== null) { clearTimeout(this._progressIndicatorTimer); this._progressIndicatorTimer = null; @@ -901,10 +902,10 @@ export class Display extends EventDispatcher { async _onKanjiLookup(e) { try { e.preventDefault(); - const {state} = this._history; + const { state } = this._history; if (!(typeof state === 'object' && state !== null)) { return; } - let {sentence, url, documentTitle} = state; + let { sentence, url, documentTitle } = state; if (typeof url !== 'string') { url = window.location.href; } if (typeof documentTitle !== 'string') { documentTitle = document.title; } const optionsContext = this.getOptionsContext(); @@ -1019,7 +1020,7 @@ export class Display extends EventDispatcher { _onEntryClick(e) { if (e.button !== 0) { return; } const node = /** @type {HTMLElement} */ (e.currentTarget); - const {index} = node.dataset; + const { index } = node.dataset; if (typeof index !== 'string') { return; } const indexNumber = parseInt(index, 10); if (!Number.isFinite(indexNumber)) { return; } @@ -1066,7 +1067,7 @@ export class Display extends EventDispatcher { */ _onMenuButtonMenuClose(e) { const node = /** @type {HTMLElement} */ (e.currentTarget); - const {action} = e.detail; + const { action } = e.detail; switch (action) { case 'log-debug-info': this._logDictionaryEntryData(this.getElementDictionaryEntryIndex(node)); @@ -1127,8 +1128,8 @@ export class Display extends EventDispatcher { * @param {import('settings').ProfileOptions} options */ _setTheme(options) { - const {general} = options; - const {popupTheme} = general; + const { general } = options; + const { popupTheme } = general; this._themeController.theme = popupTheme; this._themeController.outerTheme = general.popupOuterTheme; this._themeController.updateTheme(); @@ -1163,8 +1164,87 @@ export class Display extends EventDispatcher { } } - const {dictionaryEntries} = await yomitan.api.termsFind(source, findDetails, optionsContext); + /* https://github.com/seth-js/yomichan-de */ + /** + * @type {string[]} + */ + const matchedDefs = []; + /** + * @type {any[] | PromiseLike} + */ + const dictionaryEntries = []; + + /** + * + * @param {string} text + * @returns {string} + */ + function firstCharLower(text) { + /** + * @type {string[]} + */ + let chars = []; + + text.split('').forEach((char) => chars.push(char)); + + chars[0] = chars[0].toLowerCase(); + + return chars.join(''); + } + + /** + * + * @param {string} text + * @returns {string} + */ + function firstCharUpper(text) { + /** + * @type {string[]} + */ + let chars = []; + + text.split('').forEach((char) => chars.push(char)); + + chars[0] = chars[0].toUpperCase(); + + return chars.join(''); + } + + const searches = [ + firstCharLower(source), + firstCharUpper(source), + source.toLowerCase(), + source, + ]; + + // handle english apostrophe + if (/'|´/.test(source) && !/^'|^´/.test(source)) { + const noApostrophe = source.replace(/'.+/, '').replace(/´.+/, ''); + searches.push(...[firstCharLower(noApostrophe), firstCharUpper(noApostrophe), noApostrophe.toLowerCase()]); + } + + for (const search of searches) { + const result = await yomitan.api.termsFind( + search, + findDetails, + optionsContext, + ); + + if (result.dictionaryEntries.length > 0) { + result.dictionaryEntries.forEach((entry) => { + const { definitions } = entry; + + // avoid duplicate results + if (!matchedDefs.includes(JSON.stringify(definitions))) { + matchedDefs.push(JSON.stringify(definitions)); + dictionaryEntries.push(entry); + } + }); + } + } + return dictionaryEntries; + } } @@ -1190,7 +1270,7 @@ export class Display extends EventDispatcher { } this._setQuery(query, queryFull, queryOffset); - let {state, content} = this._history; + let { state, content } = this._history; let changeHistory = false; if (!(typeof content === 'object' && content !== null)) { content = {}; @@ -1201,7 +1281,7 @@ export class Display extends EventDispatcher { changeHistory = true; } - let {focusEntry, scrollX, scrollY, optionsContext} = state; + let { focusEntry, scrollX, scrollY, optionsContext } = state; if (typeof focusEntry !== 'number') { focusEntry = 0; } if (!(typeof optionsContext === 'object' && optionsContext !== null)) { optionsContext = this.getOptionsContext(); @@ -1209,7 +1289,7 @@ export class Display extends EventDispatcher { changeHistory = true; } - let {dictionaryEntries} = content; + let { dictionaryEntries } = content; if (!Array.isArray(dictionaryEntries)) { dictionaryEntries = lookup && query.length > 0 ? await this._findDictionaryEntries(type === 'kanji', query, wildcardsEnabled, optionsContext) : []; if (this._setContentToken !== token) { return; } @@ -1218,9 +1298,9 @@ export class Display extends EventDispatcher { } let contentOriginValid = false; - const {contentOrigin} = content; + const { contentOrigin } = content; if (typeof contentOrigin === 'object' && contentOrigin !== null) { - const {tabId, frameId} = contentOrigin; + const { tabId, frameId } = contentOrigin; if (typeof tabId === 'number' && typeof frameId === 'number') { this._contentOriginTabId = tabId; this._contentOriginFrameId = frameId; @@ -1263,8 +1343,8 @@ export class Display extends EventDispatcher { const dictionaryEntry = dictionaryEntries[i]; const entry = ( dictionaryEntry.type === 'term' ? - this._displayGenerator.createTermEntry(dictionaryEntry) : - this._displayGenerator.createKanjiEntry(dictionaryEntry) + this._displayGenerator.createTermEntry(dictionaryEntry) : + this._displayGenerator.createKanjiEntry(dictionaryEntry) ); entry.dataset.index = `${i}`; this._dictionaryEntryNodes.push(entry); @@ -1279,13 +1359,113 @@ export class Display extends EventDispatcher { } if (typeof scrollX === 'number' || typeof scrollY === 'number') { - let {x, y} = this._windowScroll; + let { x, y } = this._windowScroll; if (typeof scrollX === 'number') { x = scrollX; } if (typeof scrollY === 'number') { y = scrollY; } this._windowScroll.stop(); this._windowScroll.to(x, y); } + /* https://github.com/seth-js/yomichan-de */ + for (const entryElem of Array.from(document.querySelectorAll('#dictionary-entries .entry'))) { + const formBoxes = {}; + + for (const inflectElem of entryElem.querySelectorAll('.inflection')) { + if (inflectElem == undefined) return; + if (inflectElem.textContent == undefined) return; + + const [targetPOS] = inflectElem.textContent.split(' '); + + let _inflectText = inflectElem.textContent.split(' '); + _inflectText.shift(); + let inflectText = _inflectText.join(' '); + + if (!formBoxes[targetPOS]) formBoxes[targetPOS] = {}; + if (!formBoxes[targetPOS]['inflections']) + formBoxes[targetPOS]['inflections'] = []; + formBoxes[targetPOS]['isAutomated'] = false; + + if (/-automated-/.test(inflectText)) { + inflectText = inflectText.replace(/^-.+?- /, ''); + formBoxes[targetPOS]['isAutomated'] = true; + } + + const pointerText = inflectText.replace(/\}.+/, '').replace(/\{/, ''); + inflectText = inflectText.replace(/\{.+?\} /, ''); + + formBoxes[targetPOS]['pointerText'] = pointerText; + + formBoxes[targetPOS]['inflections'].push(inflectText); + } + + for (const defElem of Array.from(entryElem.querySelectorAll('.definition-item'))) { + for (const tagElem of Array.from(defElem.querySelectorAll('.tag[data-category="partOfSpeech"]'))) { + const pos = tagElem.textContent; + if (pos == null) return; + if (formBoxes[pos]) { + const formInfoBox = document.createElement('div'); + + formInfoBox.classList.add('form-info-box'); + + if (formBoxes[pos].isAutomated) { + const automatedNotice = document.createElement('div'); + automatedNotice.classList.add('automated-result-text'); + automatedNotice.textContent = '(automated results)'; + formInfoBox.appendChild(automatedNotice); + } + + const pointerElem = document.createElement('div'); + pointerElem.classList.add('pointer-text'); + pointerElem.textContent = formBoxes[pos].pointerText; + formInfoBox.appendChild(pointerElem); + + const reasonList = document.createElement('ol'); + + for (const reason of formBoxes[pos].inflections) { + const item = document.createElement('li'); + item.textContent = reason; + reasonList.appendChild(item); + } + + formInfoBox.appendChild(reasonList); + + const definitionTagList = defElem.querySelector('.definition-tag-list'); + if (definitionTagList == undefined) return; + definitionTagList.append(formInfoBox); + } + } + + if (defElem.querySelector('.form-info-box')) { + defElem.addEventListener('mouseleave', (e) => { + for (const boxElem of Array.from(defElem.querySelectorAll('.form-info-box'))) { + // Element does not have a 'style' property, but HTMLElement does implement it + // @ts-ignore + boxElem.style.display = 'none'; + } + }); + + const showBoxIcon = document.createElement('span'); + showBoxIcon.textContent = 'ⓘ'; + showBoxIcon.classList.add('show-info-btn'); + + showBoxIcon.addEventListener('mouseenter', (e) => { + for (const boxElem of Array.from(defElem.querySelectorAll('.form-info-box'))) { + // Element does not have a 'style' property, but HTMLElement does implement it + // @ts-ignore + boxElem.style.display = 'block'; + } + }); + + let formIntoBox = defElem.querySelector('.form-info-box'); + if (formIntoBox == undefined) return; + formIntoBox.before(showBoxIcon); + } + } + } + + // ============================== + + this._triggerContentUpdateComplete(); } @@ -1394,7 +1574,7 @@ export class Display extends EventDispatcher { * @param {boolean} next */ _updateNavigation(previous, next) { - const {documentElement} = document; + const { documentElement } = document; if (documentElement !== null) { documentElement.dataset.hasNavigationPrevious = `${previous}`; documentElement.dataset.hasNavigationNext = `${next}`; @@ -1474,11 +1654,11 @@ export class Display extends EventDispatcher { let focusDefinitionIndex = null; if (dictionaryEntry.type === 'term') { - const {dictionary} = dictionaryEntry.definitions[visibleDefinitionIndex]; + const { dictionary } = dictionaryEntry.definitions[visibleDefinitionIndex]; for (let i = index; i >= 0 && i < count; i += sign) { const otherDictionaryEntry = this._dictionaryEntries[i]; if (otherDictionaryEntry.type !== 'term') { continue; } - const {definitions} = otherDictionaryEntry; + const { definitions } = otherDictionaryEntry; const jj = definitions.length; let j = (i === index ? visibleDefinitionIndex + sign : (sign > 0 ? 0 : jj - 1)); for (; j >= 0 && j < jj; j += sign) { @@ -1504,9 +1684,9 @@ export class Display extends EventDispatcher { * @returns {?number} */ _getDictionaryEntryVisibleDefinitionIndex(index, sign) { - const {top: scrollTop, bottom: scrollBottom} = this._windowScroll.getRect(); + const { top: scrollTop, bottom: scrollBottom } = this._windowScroll.getRect(); - const {definitions} = this._dictionaryEntries[index]; + const { definitions } = this._dictionaryEntries[index]; const nodes = this._getDictionaryEntryDefinitionNodes(index); const definitionCount = Math.min(definitions.length, nodes.length); if (definitionCount <= 0) { return null; } @@ -1514,7 +1694,7 @@ export class Display extends EventDispatcher { let visibleIndex = null; let visibleCoverage = 0; for (let i = (sign > 0 ? 0 : definitionCount - 1); i >= 0 && i < definitionCount; i += sign) { - const {top, bottom} = nodes[i].getBoundingClientRect(); + const { top, bottom } = nodes[i].getBoundingClientRect(); if (bottom <= scrollTop || top >= scrollBottom) { continue; } const top2 = Math.max(scrollTop, Math.min(scrollBottom, top)); const bottom2 = Math.max(scrollTop, Math.min(scrollBottom, bottom)); @@ -1579,7 +1759,7 @@ export class Display extends EventDispatcher { /** */ _updateHistoryState() { - const {state, content} = this._history; + const { state, content } = this._history; if (!(typeof state === 'object' && state !== null)) { return; } state.focusEntry = this._index; @@ -1639,8 +1819,8 @@ export class Display extends EventDispatcher { _isQueryParserVisible() { return ( this._queryParserVisibleOverride !== null ? - this._queryParserVisibleOverride : - this._queryParserVisible + this._queryParserVisibleOverride : + this._queryParserVisible ); } @@ -1678,8 +1858,8 @@ export class Display extends EventDispatcher { typeof this._tabId === 'number' && ( (isSearchPage) ? - (options.scanning.enableOnSearchPage) : - (this._depth < options.scanning.popupNestingMaxDepth) + (options.scanning.enableOnSearchPage) : + (this._depth < options.scanning.popupNestingMaxDepth) ) ); @@ -1855,12 +2035,12 @@ export class Display extends EventDispatcher { this._contentTextScanner.on('searched', this._onContentTextScannerSearched.bind(this)); } - const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options; + const { scanning: scanningOptions, sentenceParsing: sentenceParsingOptions } = options; this._contentTextScanner.setOptions({ inputs: [{ include: 'mouse0', exclude: '', - types: {mouse: true, pen: false, touch: false}, + types: { mouse: true, pen: false, touch: false }, options: { searchTerms: true, searchKanji: true, @@ -1899,7 +2079,7 @@ export class Display extends EventDispatcher { /** * @param {import('text-scanner').SearchedEventDetails} details */ - _onContentTextScannerSearched({type, dictionaryEntries, sentence, textSource, optionsContext, error}) { + _onContentTextScannerSearched({ type, dictionaryEntries, sentence, textSource, optionsContext, error }) { if (error !== null && !yomitan.isExtensionUnloaded) { log.error(error); } @@ -1938,7 +2118,7 @@ export class Display extends EventDispatcher { * @type {import('display').GetSearchContextCallback} */ _getSearchContext() { - return {optionsContext: this.getOptionsContext()}; + return { optionsContext: this.getOptionsContext() }; } /** @@ -2014,12 +2194,12 @@ export class Display extends EventDispatcher { async _logDictionaryEntryData(index) { if (index < 0 || index >= this._dictionaryEntries.length) { return; } const dictionaryEntry = this._dictionaryEntries[index]; - const result = {dictionaryEntry}; + const result = { dictionaryEntry }; /** @type {Promise[]} */ const promises = []; /** @type {import('display').LogDictionaryEntryDataEvent} */ - const event = {dictionaryEntry, promises}; + const event = { dictionaryEntry, promises }; this.trigger('logDictionaryEntryData', event); if (promises.length > 0) { for (const result2 of await Promise.all(promises)) { @@ -2038,7 +2218,7 @@ export class Display extends EventDispatcher { /** */ _triggerContentUpdateStart() { /** @type {import('display').ContentUpdateStartEvent} */ - const event = {type: this._contentType, query: this._query}; + const event = { type: this._contentType, query: this._query }; this.trigger('contentUpdateStart', event); } @@ -2049,14 +2229,14 @@ export class Display extends EventDispatcher { */ _triggerContentUpdateEntry(dictionaryEntry, element, index) { /** @type {import('display').ContentUpdateEntryEvent} */ - const event = {dictionaryEntry, element, index}; + const event = { dictionaryEntry, element, index }; this.trigger('contentUpdateEntry', event); } /** */ _triggerContentUpdateComplete() { /** @type {import('display').ContentUpdateCompleteEvent} */ - const event = {type: this._contentType}; + const event = { type: this._contentType }; this.trigger('contentUpdateComplete', event); } } diff --git a/ext/js/language/dictionary-database.js b/ext/js/language/dictionary-database.js index c47e1e902f..75965d048e 100644 --- a/ext/js/language/dictionary-database.js +++ b/ext/js/language/dictionary-database.js @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -import {log, stringReverse} from '../core.js'; -import {Database} from '../data/database.js'; +import { log, stringReverse } from '../core.js'; +import { Database } from '../data/database.js'; export class DictionaryDatabase { constructor() { @@ -63,19 +63,19 @@ export class DictionaryDatabase { version: 20, stores: { terms: { - primaryKey: {keyPath: 'id', autoIncrement: true}, + primaryKey: { keyPath: 'id', autoIncrement: true }, indices: ['dictionary', 'expression', 'reading'] }, kanji: { - primaryKey: {autoIncrement: true}, + primaryKey: { autoIncrement: true }, indices: ['dictionary', 'character'] }, tagMeta: { - primaryKey: {autoIncrement: true}, + primaryKey: { autoIncrement: true }, indices: ['dictionary'] }, dictionaries: { - primaryKey: {autoIncrement: true}, + primaryKey: { autoIncrement: true }, indices: ['title', 'version'] } } @@ -84,15 +84,15 @@ export class DictionaryDatabase { version: 30, stores: { termMeta: { - primaryKey: {autoIncrement: true}, + primaryKey: { autoIncrement: true }, indices: ['dictionary', 'expression'] }, kanjiMeta: { - primaryKey: {autoIncrement: true}, + primaryKey: { autoIncrement: true }, indices: ['dictionary', 'character'] }, tagMeta: { - primaryKey: {autoIncrement: true}, + primaryKey: { autoIncrement: true }, indices: ['dictionary', 'name'] } } @@ -101,7 +101,7 @@ export class DictionaryDatabase { version: 40, stores: { terms: { - primaryKey: {keyPath: 'id', autoIncrement: true}, + primaryKey: { keyPath: 'id', autoIncrement: true }, indices: ['dictionary', 'expression', 'reading', 'sequence'] } } @@ -110,7 +110,7 @@ export class DictionaryDatabase { version: 50, stores: { terms: { - primaryKey: {keyPath: 'id', autoIncrement: true}, + primaryKey: { keyPath: 'id', autoIncrement: true }, indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse'] } } @@ -119,7 +119,7 @@ export class DictionaryDatabase { version: 60, stores: { media: { - primaryKey: {keyPath: 'id', autoIncrement: true}, + primaryKey: { keyPath: 'id', autoIncrement: true }, indices: ['dictionary', 'path'] } } @@ -235,7 +235,7 @@ export class DictionaryDatabase { /** @type {import('dictionary-database').FindPredicate} */ const predicate = (row) => { if (!dictionaries.has(row.dictionary)) { return false; } - const {id} = row; + const { id } = row; if (visited.has(id)) { return false; } visited.add(id); return true; @@ -373,19 +373,19 @@ export class DictionaryDatabase { const databaseTargets = targets.map(([objectStoreName, indexName]) => { const objectStore = transaction.objectStore(objectStoreName); const index = objectStore.index(indexName); - return {objectStore, index}; + return { objectStore, index }; }); /** @type {import('database').CountTarget[]} */ const countTargets = []; if (getTotal) { - for (const {objectStore} of databaseTargets) { + for (const { objectStore } of databaseTargets) { countTargets.push([objectStore, void 0]); } } for (const dictionaryName of dictionaryNames) { const query = IDBKeyRange.only(dictionaryName); - for (const {index} of databaseTargets) { + for (const { index } of databaseTargets) { countTargets.push([index, query]); } } @@ -407,7 +407,7 @@ export class DictionaryDatabase { counts.push(countGroup); } const total = getTotal ? /** @type {import('dictionary-database').DictionaryCountGroup} */ (counts.shift()) : null; - resolve({total, counts}); + resolve({ total, counts }); }; this._db.bulkCount(countTargets, onCountComplete, reject); @@ -488,7 +488,7 @@ export class DictionaryDatabase { const query = createQuery(item); for (let j = 0; j < indexCount; ++j) { /** @type {import('dictionary-database').FindMultiBulkData} */ - const data = {item, itemIndex: i, indexIndex: j}; + const data = { item, itemIndex: i, indexIndex: j }; this._db.getAll(indexList[j], query, onGetAll, reject, data); } } @@ -578,7 +578,7 @@ export class DictionaryDatabase { * @returns {import('dictionary-database').TermEntry} */ _createTerm(matchSource, matchType, row, index) { - const {sequence} = row; + const { sequence } = row; return { index, matchType, @@ -592,7 +592,8 @@ export class DictionaryDatabase { score: row.score, dictionary: row.dictionary, id: row.id, - sequence: typeof sequence === 'number' ? sequence : -1 + sequence: typeof sequence === 'number' ? sequence : -1, + skip: false, }; } @@ -601,8 +602,8 @@ export class DictionaryDatabase { * @param {import('dictionary-database').FindMultiBulkData} data * @returns {import('dictionary-database').KanjiEntry} */ - _createKanji(row, {itemIndex: index}) { - const {stats} = row; + _createKanji(row, { itemIndex: index }) { + const { stats } = row; return { index, character: row.character, @@ -621,12 +622,12 @@ export class DictionaryDatabase { * @returns {import('dictionary-database').TermMeta} * @throws {Error} */ - _createTermMeta({expression: term, mode, data, dictionary}, {itemIndex: index}) { + _createTermMeta({ expression: term, mode, data, dictionary }, { itemIndex: index }) { switch (mode) { case 'freq': - return {index, term, mode, data, dictionary}; + return { index, term, mode, data, dictionary }; case 'pitch': - return {index, term, mode, data, dictionary}; + return { index, term, mode, data, dictionary }; default: throw new Error(`Unknown mode: ${mode}`); } @@ -637,8 +638,8 @@ export class DictionaryDatabase { * @param {import('dictionary-database').FindMultiBulkData} data * @returns {import('dictionary-database').KanjiMeta} */ - _createKanjiMeta({character, mode, data, dictionary}, {itemIndex: index}) { - return {index, character, mode, data, dictionary}; + _createKanjiMeta({ character, mode, data, dictionary }, { itemIndex: index }) { + return { index, character, mode, data, dictionary }; } /** @@ -646,9 +647,9 @@ export class DictionaryDatabase { * @param {import('dictionary-database').FindMultiBulkData} data * @returns {import('dictionary-database').Media} */ - _createMedia(row, {itemIndex: index}) { - const {dictionary, path, mediaType, width, height, content} = row; - return {index, dictionary, path, mediaType, width, height, content}; + _createMedia(row, { itemIndex: index }) { + const { dictionary, path, mediaType, width, height, content } = row; + return { index, dictionary, path, mediaType, width, height, content }; } /** diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js index d1b033e6f2..24d5608e42 100644 --- a/ext/js/language/text-scanner.js +++ b/ext/js/language/text-scanner.js @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2019-2022 Yomichan Authors * @@ -16,10 +17,10 @@ * along with this program. If not, see . */ -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'; +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'; /** * @augments EventDispatcher @@ -31,12 +32,12 @@ export class TextScanner extends EventDispatcher { constructor({ node, getSearchContext, - ignoreElements=null, - ignorePoint=null, - searchTerms=false, - searchKanji=false, - searchOnClick=false, - searchOnClickOnly=false + ignoreElements = null, + ignorePoint = null, + searchTerms = false, + searchKanji = false, + searchOnClick = false, + searchOnClickOnly = false }) { super(); /** @type {HTMLElement|Window} */ @@ -270,7 +271,7 @@ export class TextScanner extends EventDispatcher { this._matchTypePrefix = matchTypePrefix; } if (typeof sentenceParsingOptions === 'object' && sentenceParsingOptions !== null) { - const {scanExtent, terminationCharacterMode, terminationCharacters} = sentenceParsingOptions; + const { scanExtent, terminationCharacterMode, terminationCharacters } = sentenceParsingOptions; if (typeof scanExtent === 'number') { this._sentenceScanExtent = scanExtent; } @@ -287,7 +288,7 @@ export class TextScanner extends EventDispatcher { Array.isArray(terminationCharacters) && (terminationCharacterMode === 'custom' || terminationCharacterMode === 'custom-no-newlines') ) { - for (const {enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd} of terminationCharacters) { + for (const { enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd } of terminationCharacters) { if (!enabled) { continue; } if (character2 === null) { sentenceTerminatorMap.set(character1, [includeCharacterAtStart, includeCharacterAtEnd]); @@ -403,7 +404,7 @@ export class TextScanner extends EventDispatcher { */ _createOptionsContextForInput(baseOptionsContext, inputInfo) { const optionsContext = clone(baseOptionsContext); - const {modifiers, modifierKeys} = inputInfo; + const { modifiers, modifierKeys } = inputInfo; optionsContext.modifiers = [...modifiers]; optionsContext.modifierKeys = [...modifierKeys]; return optionsContext; @@ -435,8 +436,8 @@ export class TextScanner extends EventDispatcher { const inputInfoDetail = inputInfo.detail; const selectionRestoreInfo = ( (typeof inputInfoDetail === 'object' && inputInfoDetail !== null && inputInfoDetail.restoreSelection) ? - (this._inputInfoCurrent === null ? this._createSelectionRestoreInfo() : null) : - null + (this._inputInfoCurrent === null ? this._createSelectionRestoreInfo() : null) : + null ); if (this._textSourceCurrent !== null && this._textSourceCurrent.hasSameStart(textSource)) { @@ -445,7 +446,7 @@ export class TextScanner extends EventDispatcher { const getSearchContextPromise = this._getSearchContext(); const getSearchContextResult = getSearchContextPromise instanceof Promise ? await getSearchContextPromise : getSearchContextPromise; - const {detail: detail2} = getSearchContextResult; + const { detail: detail2 } = getSearchContextResult; if (typeof detail2 !== 'undefined') { detail = detail2; } optionsContext = this._createOptionsContextForInput(getSearchContextResult.optionsContext, inputInfo); @@ -454,11 +455,11 @@ export class TextScanner extends EventDispatcher { let valid = false; const result = await this._findDictionaryEntries(textSource, searchTerms, searchKanji, optionsContext); if (result !== null) { - ({dictionaryEntries, sentence, type} = result); + ({ dictionaryEntries, sentence, type } = result); valid = true; } else if (textSource !== null && textSource instanceof TextSourceElement && await this._hasJapanese(textSource.fullContent)) { dictionaryEntries = []; - sentence = {text: '', offset: 0}; + sentence = { text: '', offset: 0 }; type = 'terms'; valid = true; } @@ -533,7 +534,7 @@ export class TextScanner extends EventDispatcher { * @param {MouseEvent} e */ _onMouseOver(e) { - if (this._ignoreElements !== null && this._ignoreElements().includes(/** @type {Element} */ (e.target))) { + if (this._ignoreElements !== null && this._ignoreElements().includes(/** @type {Element} */(e.target))) { this._scanTimerClear(); } } @@ -646,7 +647,7 @@ export class TextScanner extends EventDispatcher { return; } - const {clientX, clientY, identifier} = e.changedTouches[0]; + const { clientX, clientY, identifier } = e.changedTouches[0]; this._onPrimaryTouchStart(e, clientX, clientY, identifier); } @@ -686,7 +687,7 @@ export class TextScanner extends EventDispatcher { const primaryTouch = this._getTouch(e.changedTouches, this._primaryTouchIdentifier); if (primaryTouch === null) { return; } - const {clientX, clientY} = primaryTouch; + const { clientX, clientY } = primaryTouch; this._onPrimaryTouchEnd(e, clientX, clientY, true); } @@ -742,7 +743,7 @@ export class TextScanner extends EventDispatcher { const inputInfo = this._getMatchingInputGroupFromEvent('touch', 'touchMove', e); if (inputInfo === null) { return; } - const {input} = inputInfo; + const { input } = inputInfo; if (input !== null && input.scanOnTouchMove) { this._searchAt(primaryTouch.clientX, primaryTouch.clientY, inputInfo); } @@ -755,7 +756,7 @@ export class TextScanner extends EventDispatcher { * @returns {boolean|void} */ _onPointerOver(e) { - const {pointerType, pointerId, isPrimary} = e; + const { pointerType, pointerId, isPrimary } = e; if (pointerType === 'pen') { this._pointerIdTypeMap.set(pointerId, pointerType); } @@ -888,7 +889,7 @@ export class TextScanner extends EventDispatcher { * @returns {boolean|void} */ _onTouchPointerDown(e) { - const {clientX, clientY, pointerId} = e; + const { clientX, clientY, pointerId } = e; this._onPrimaryTouchStart(e, clientX, clientY, pointerId); } @@ -912,7 +913,7 @@ export class TextScanner extends EventDispatcher { * @returns {boolean|void} */ _onTouchPointerUp(e) { - const {clientX, clientY} = e; + const { clientX, clientY } = e; return this._onPrimaryTouchEnd(e, clientX, clientY, true); } @@ -1071,7 +1072,7 @@ export class TextScanner extends EventDispatcher { [this._node, 'pointerup', this._onPointerUp.bind(this), capture], [this._node, 'pointercancel', this._onPointerCancel.bind(this), capture], [this._node, 'pointerout', this._onPointerOut.bind(this), capture], - [this._node, 'touchmove', this._onTouchMovePreventScroll.bind(this), {passive: false, capture}], + [this._node, 'touchmove', this._onTouchMovePreventScroll.bind(this), { passive: false, capture }], [this._node, 'mousedown', this._onMouseDown.bind(this), capture], [this._node, 'click', this._onClick.bind(this), capture], [this._node, 'auxclick', this._onAuxClick.bind(this), capture] @@ -1102,7 +1103,7 @@ export class TextScanner extends EventDispatcher { [this._node, 'touchstart', this._onTouchStart.bind(this), capture], [this._node, 'touchend', this._onTouchEnd.bind(this), capture], [this._node, 'touchcancel', this._onTouchCancel.bind(this), capture], - [this._node, 'touchmove', this._onTouchMove.bind(this), {passive: false, capture}], + [this._node, 'touchmove', this._onTouchMove.bind(this), { passive: false, capture }], [this._node, 'contextmenu', this._onContextMenu.bind(this), capture] ]; } @@ -1122,7 +1123,7 @@ export class TextScanner extends EventDispatcher { * @returns {import('event-listener-collection').AddEventListenerArgs[]} */ _getMouseClickOnlyEventListeners2(capture) { - const {documentElement} = document; + const { documentElement } = document; /** @type {import('event-listener-collection').AddEventListenerArgs[]} */ const entries = [ [document, 'selectionchange', this._onSelectionChange.bind(this)] @@ -1198,7 +1199,72 @@ export class TextScanner extends EventDispatcher { /** @type {import('api').FindTermsDetails} */ const details = {}; if (this._matchTypePrefix) { details.matchType = 'prefix'; } - const {dictionaryEntries, originalTextLength} = await yomitan.api.termsFind(searchText, details, optionsContext); + + // const {dictionaryEntries, originalTextLength} = await yomitan.api.termsFind(searchText, details, optionsContext); + // if (dictionaryEntries.length === 0) { return null; } + + /* https://github.com/seth-js/yomichan-de */ + /** @type {string[]} */ + const matchedDefs = []; + /** @type {import('dictionary').TermDictionaryEntry[]} */ + const dictionaryEntries = []; + let originalTextLength = 0; + + /** + * @param {string} text + * @returns {string} + */ + function firstCharLower(text) { + /** @type {string[]} */ + let chars = []; + + text.split('').forEach((char) => chars.push(char)); + + chars[0] = chars[0].toLowerCase(); + + return chars.join(''); + } + + /** + * @param {string} text + * @returns {string} + */ + function firstCharUpper(text) { + /** @type {string[]} */ + let chars = []; + + text.split('').forEach((char) => chars.push(char)); + + chars[0] = chars[0].toUpperCase(); + + return chars.join(''); + } + + const searches = [firstCharLower(searchText), firstCharUpper(searchText), searchText.toLowerCase(), searchText]; + + // handle english apostrophe + if (/'|´/.test(searchText) && !/^'|^´/.test(searchText)) { + const noApostrophe = searchText.replace(/'.+/, '').replace(/´.+/, ''); + searches.push(...[firstCharLower(noApostrophe), firstCharUpper(noApostrophe), noApostrophe.toLowerCase()]); + } + + for (const search of searches) { + const result = await yomitan.api.termsFind(search, details, optionsContext); + + if (result.dictionaryEntries.length > 0) { + result.dictionaryEntries.forEach((entry) => { + const { definitions } = entry; + + // avoid duplicate results + if (!matchedDefs.includes(JSON.stringify(definitions))) { + matchedDefs.push(JSON.stringify(definitions)); + dictionaryEntries.push(entry); + originalTextLength = result.originalTextLength; + } + }); + } + } + if (dictionaryEntries.length === 0) { return null; } textSource.setEndOffset(originalTextLength, false, layoutAwareScan); @@ -1212,7 +1278,7 @@ export class TextScanner extends EventDispatcher { sentenceBackwardQuoteMap ); - return {dictionaryEntries, sentence, type: 'terms'}; + return { dictionaryEntries, sentence, type: 'terms' }; } /** @@ -1244,7 +1310,7 @@ export class TextScanner extends EventDispatcher { sentenceBackwardQuoteMap ); - return {dictionaryEntries, sentence, type: 'kanji'}; + return { dictionaryEntries, sentence, type: 'kanji' }; } /** @@ -1316,7 +1382,7 @@ export class TextScanner extends EventDispatcher { */ async _searchAtFromTouchStart(x, y, inputInfo) { const textSourceCurrentPrevious = this._textSourceCurrent !== null ? this._textSourceCurrent.clone() : null; - const {input} = inputInfo; + const { input } = inputInfo; const preventScroll = input !== null && input.preventTouchScrolling; await this._searchAt(x, y, inputInfo); @@ -1351,7 +1417,7 @@ export class TextScanner extends EventDispatcher { const inputInfo = this._getMatchingInputGroupFromEvent('pen', eventType, e); if (inputInfo === null) { return; } - const {input} = inputInfo; + const { input } = inputInfo; if (input === null || !this._isPenEventSupported(eventType, input)) { return; } const preventScroll = input !== null && input.preventPenScrolling; @@ -1417,7 +1483,7 @@ export class TextScanner extends EventDispatcher { const modifiersSet = new Set(modifiers); for (let i = 0, ii = this._inputs.length; i < ii; ++i) { const input = this._inputs[i]; - const {include, exclude, types} = input; + const { include, exclude, types } = input; if (!types.has(pointerType)) { continue; } if (this._setHasAll(modifiersSet, include) && (exclude.length === 0 || !this._setHasAll(modifiersSet, exclude))) { if (include.length > 0) { @@ -1430,8 +1496,8 @@ export class TextScanner extends EventDispatcher { return ( fallbackIndex >= 0 ? - this._createInputInfo(this._inputs[fallbackIndex], pointerType, eventType, true, modifiers, modifierKeys) : - null + this._createInputInfo(this._inputs[fallbackIndex], pointerType, eventType, true, modifiers, modifierKeys) : + null ); } @@ -1446,7 +1512,7 @@ export class TextScanner extends EventDispatcher { * @returns {import('text-scanner').InputInfo} */ _createInputInfo(input, pointerType, eventType, passive, modifiers, modifierKeys, detail) { - return {input, pointerType, eventType, passive, modifiers, modifierKeys, detail}; + return { input, pointerType, eventType, passive, modifiers, modifierKeys, detail }; } /** @@ -1468,7 +1534,7 @@ export class TextScanner extends EventDispatcher { * @returns {import('text-scanner').InputConfig} */ _convertInput(input) { - const {options} = input; + const { options } = input; return { include: this._getInputArray(input.include), exclude: this._getInputArray(input.exclude), @@ -1495,8 +1561,8 @@ export class TextScanner extends EventDispatcher { _getInputArray(value) { return ( typeof value === 'string' ? - value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0) : - [] + value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0) : + [] ); } @@ -1504,7 +1570,7 @@ export class TextScanner extends EventDispatcher { * @param {{mouse: boolean, touch: boolean, pen: boolean}} details * @returns {Set<'mouse'|'touch'|'pen'>} */ - _getInputTypeSet({mouse, touch, pen}) { + _getInputTypeSet({ mouse, touch, pen }) { const set = new Set(); if (mouse) { set.add('mouse'); } if (touch) { set.add('touch'); } @@ -1576,14 +1642,14 @@ export class TextScanner extends EventDispatcher { ranges.push(range.cloneRange()); } } - return {ranges}; + return { ranges }; } /** * @param {import('text-scanner').SelectionRestoreInfo} selectionRestoreInfo */ _restoreSelection(selectionRestoreInfo) { - const {ranges} = selectionRestoreInfo; + const { ranges } = selectionRestoreInfo; const selection = window.getSelection(); if (selection === null) { return; } selection.removeAllRanges(); @@ -1600,7 +1666,7 @@ export class TextScanner extends EventDispatcher { * @param {string} reason */ _triggerClear(reason) { - this.trigger('clear', {reason}); + this.trigger('clear', { reason }); } /** diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index aa1b71dd57..618f867526 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2016-2022 Yomichan Authors * @@ -16,9 +17,9 @@ * along with this program. If not, see . */ -import {RegexUtil} from '../general/regex-util.js'; -import {TextSourceMap} from '../general/text-source-map.js'; -import {Deinflector} from './deinflector.js'; +import { RegexUtil } from '../general/regex-util.js'; +import { TextSourceMap } from '../general/text-source-map.js'; +import { Deinflector } from './deinflector.js'; /** * Class which finds term and kanji dictionary entries for text. @@ -28,7 +29,7 @@ export class Translator { * Creates a new Translator instance. * @param {import('translator').ConstructorDetails} details The details for the class. */ - constructor({japaneseUtil, database}) { + constructor({ japaneseUtil, database }) { /** @type {import('./sandbox/japanese-util.js').JapaneseUtil} */ this._japaneseUtil = japaneseUtil; /** @type {import('./dictionary-database.js').DictionaryDatabase} */ @@ -68,9 +69,9 @@ export class Translator { * @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; + const { enabledDictionaryMap, excludeDictionaryDefinitions, sortFrequencyDictionary, sortFrequencyDictionaryOrder } = options; const tagAggregator = new TranslatorTagAggregator(); - let {dictionaryEntries, originalTextLength} = await this._findTermsInternal(text, enabledDictionaryMap, options, tagAggregator); + let { dictionaryEntries, originalTextLength } = await this._findTermsInternal(text, enabledDictionaryMap, options, tagAggregator); switch (mode) { case 'group': @@ -106,14 +107,14 @@ export class Translator { if (dictionaryEntries.length > 1) { this._sortTermDictionaryEntries(dictionaryEntries); } - for (const {definitions, frequencies, pronunciations} of dictionaryEntries) { + for (const { definitions, frequencies, pronunciations } of dictionaryEntries) { this._flagRedundantDefinitionTags(definitions); if (definitions.length > 1) { this._sortTermDictionaryEntryDefinitions(definitions); } if (frequencies.length > 1) { this._sortTermDictionaryEntrySimpleData(frequencies); } if (pronunciations.length > 1) { this._sortTermDictionaryEntrySimpleData(pronunciations); } } - return {dictionaryEntries, originalTextLength}; + return { dictionaryEntries, originalTextLength }; } /** @@ -125,7 +126,7 @@ export class Translator { * @returns {Promise} An array of definitions. See the _createKanjiDefinition() function for structure details. */ async findKanji(text, options) { - const {enabledDictionaryMap} = options; + const { enabledDictionaryMap } = options; const kanjiUnique = new Set(); for (const c of text) { kanjiUnique.add(c); @@ -139,7 +140,7 @@ export class Translator { /** @type {import('dictionary').KanjiDictionaryEntry[]} */ const dictionaryEntries = []; const tagAggregator = new TranslatorTagAggregator(); - for (const {character, onyomi, kunyomi, tags, definitions, stats, dictionary} of databaseEntries) { + for (const { character, onyomi, kunyomi, tags, definitions, stats, dictionary } of databaseEntries) { const expandedStats = await this._expandKanjiStats(stats, dictionary); const dictionaryEntry = this._createKanjiDictionaryEntry(character, dictionary, onyomi, kunyomi, expandedStats, definitions); dictionaryEntries.push(dictionaryEntry); @@ -168,21 +169,21 @@ export class Translator { dictionarySet.add(dictionary); } - const termList = termReadingList.map(({term}) => term); + 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) { + for (const { mode, data, dictionary, index } of metas) { if (mode !== 'freq') { continue; } - let {term, reading} = termReadingList[index]; + let { term, reading } = termReadingList[index]; 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); + const { frequency: frequencyValue, displayValue, displayValueParsed } = this._getFrequencyInfo(frequency); results.push({ term, reading, @@ -196,8 +197,8 @@ export class Translator { return results; } + /* https://github.com/seth-js/yomichan-de */ // Find terms internal implementation - /** * @param {string} text * @param {Map} enabledDictionaryMap @@ -210,27 +211,200 @@ export class Translator { text = this._getJapaneseOnlyText(text); } if (text.length === 0) { - return {dictionaryEntries: [], originalTextLength: 0}; + return { dictionaryEntries: [], originalTextLength: 0 }; } - const deinflections = await this._findTermsInternal2(text, enabledDictionaryMap, options); + // Makes it so that Yomichan doesn't look up parts of a word, only the full word + // Ex: находится would give me definitions for на + // Also chop the text since words with newlines or spaces after is bugging out + + // const deinflections = await this._findTermsInternal2(text, enabledDictionaryMap, options); + + const choppedText = text.replace(/\n/g, ' ').trim(); + + let deinflections = await this._findTermsInternal2(choppedText, enabledDictionaryMap, options); + + /** + * @type {import("translation-internal").DatabaseDeinflection[]} + */ + const filteredDeinflections = []; + + let smallestMatch = ''; + + deinflections.forEach(flect => { + const { originalText } = flect; + + if (!/\s/.test(originalText) + && /\p{L}$/u.test(originalText) + && !smallestMatch) + smallestMatch = originalText; + }); + + deinflections.forEach(flect => { + const { originalText, databaseEntries } = flect; + + if (databaseEntries && databaseEntries.length > 0) { + if (!smallestMatch.includes(originalText) || smallestMatch === originalText) { + filteredDeinflections.push(flect); + } + } + }); + + deinflections = [...filteredDeinflections]; + + // Automatically handle non-lemma forms by looking up what they point to + const requiredSearches = {}; + let searching = false; + + do { + searching = false; + + for (const { databaseEntries } of deinflections) { + for (const ent of databaseEntries) { + + const { definitionTags, definitions, term } = ent; + + if (definitionTags.includes('non-lemma')) { + ent.skip = true; + + for (const definition of definitions) { + const lemma = (typeof definition == 'string') ? definition.replace(/.+?\(->(?=.+?\)$)/, '').replace(/\)$/, '') : ''; + const reason = (typeof definition == 'string') ? definition.replace(/\s\(->.+/, '') : ''; + + if (!requiredSearches[lemma]) { + searching = true; + requiredSearches[lemma] = { form: term, reasons: [reason] }; + } + else if (!requiredSearches[lemma]["reasons"].includes(reason)) { + searching = true; + requiredSearches[lemma]["reasons"].push(reason); + } + } + } + } + } + + const extraDeinflections = []; + + for (const [lemma, { form, reasons }] of Object.entries(requiredSearches)) { + const flections = await this._findTermsInternal2(lemma, enabledDictionaryMap, options); + + /** + * @type {import("translation-internal").DatabaseDeinflection[]} + */ + const filteredFlections = []; + + let smallestMatch = ''; + + flections.forEach(flect => { + const { originalText } = flect; + + if (!/\s/.test(originalText) + && /\p{L}$/u.test(originalText) + && !smallestMatch) + smallestMatch = originalText; + }); + + flections.forEach(flect => { + const { originalText, deinflectedText, databaseEntries } = flect; + + if (databaseEntries && databaseEntries.length > 0 && lemma === deinflectedText) { + if (!smallestMatch.includes(originalText) || smallestMatch === originalText) { + filteredFlections.push(flect); + } + } + }); + + for (const flect of filteredFlections) { + const { databaseEntries } = flect; + + flect['originalText'] = form; + + databaseEntries.forEach(ent => { + const { definitionTags } = ent; + + if (definitionTags.includes('non-lemma')) ent.skip = true; + }); + + reasons.forEach((/** @type {string} */ reason) => { + flect.reasons.push(reason); + }); + + flect['isExtra'] = true; + extraDeinflections.push(flect); + } + } + + if (extraDeinflections.length > 0) deinflections.push(...extraDeinflections); + } while (searching); let originalTextLength = 0; const dictionaryEntries = []; const ids = new Set(); - for (const {databaseEntries, originalText, transformedText, deinflectedText, reasons} of deinflections) { + + // Added the isExtra variable so it can be checked + + // for (const {databaseEntries, originalText, transformedText, deinflectedText, reasons} of deinflections) { + + const uniqueResultsObj = {}; + const uniqueResults = []; + + for (const { databaseEntries, originalText, transformedText, deinflectedText, reasons, isExtra } of deinflections) { + if (!uniqueResultsObj[deinflectedText]) uniqueResultsObj[deinflectedText] = {}; + if (!uniqueResultsObj[deinflectedText][originalText]) uniqueResultsObj[deinflectedText][originalText] = { databaseEntries, originalText, transformedText, deinflectedText, isExtra }; + + if (reasons.length > 0) { + uniqueResultsObj[deinflectedText][originalText].reasons = [...reasons]; + } else if (!uniqueResultsObj[deinflectedText][originalText].reasons) { + uniqueResultsObj[deinflectedText][originalText].reasons = []; + } + } + + for (const { originalText, deinflectedText } of deinflections) { + if (originalText === deinflectedText && Object.entries(uniqueResultsObj[deinflectedText]).length > 1) { + delete uniqueResultsObj[deinflectedText][originalText]; + } + } + + for (const [lemma, info] of Object.entries(uniqueResultsObj)) { + const [[surface, { databaseEntries, transformedText, reasons, isExtra }]] = Object.entries(info) + uniqueResults.push({ databaseEntries, originalText: surface, transformedText, deinflectedText: lemma, reasons, isExtra }); + } + + // console.log(uniqueResults); + + for (const { databaseEntries, originalText, transformedText, deinflectedText, reasons, isExtra } of uniqueResults) { + if (databaseEntries.length === 0) { continue; } - originalTextLength = Math.max(originalTextLength, originalText.length); + + // Makes it so that the character length of lemmas don't affect the non-lemma match + // originalTextLength = Math.max(originalTextLength, originalText.length); + + if (!isExtra) { + originalTextLength = Math.max(originalTextLength, originalText.length); + } + for (const databaseEntry of databaseEntries) { - const {id} = databaseEntry; + const { id } = databaseEntry; if (ids.has(id)) { continue; } - const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, true, enabledDictionaryMap, tagAggregator); - dictionaryEntries.push(dictionaryEntry); - ids.add(id); + + // Makes it so that non-lemma entries aren't added to the dictionary entries + // We already have what they point to and the relevant form info + + // const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, true, enabledDictionaryMap); + // dictionaryEntries.push(dictionaryEntry); + // ids.add(id); + + if (!databaseEntry.skip) { + const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, true, enabledDictionaryMap, tagAggregator); + dictionaryEntries.push(dictionaryEntry); + ids.add(id); + } + } } - return {dictionaryEntries, originalTextLength}; + return { dictionaryEntries, originalTextLength }; } /** @@ -242,8 +416,8 @@ export class Translator { async _findTermsInternal2(text, enabledDictionaryMap, options) { const deinflections = ( options.deinflect ? - this._getAllDeinflections(text, options) : - [this._createDeinflection(text, text, text, 0, [])] + this._getAllDeinflections(text, options) : + [this._createDeinflection(text, text, text, 0, [])] ); if (deinflections.length === 0) { return []; } @@ -262,7 +436,7 @@ export class Translator { deinflectionArray.push(deinflection); } - const {matchType} = options; + const { matchType } = options; const databaseEntries = await this._database.findTermsBulk(uniqueDeinflectionTerms, enabledDictionaryMap, matchType); for (const databaseEntry of databaseEntries) { @@ -331,7 +505,7 @@ 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 /** @type {Deinflector} */ (this._deinflector).deinflect(source)) { + for (const { term, rules, reasons } of /** @type {Deinflector} */ (this._deinflector).deinflect(source)) { deinflections.push(this._createDeinflection(rawSource, source, term, rules, reasons)); } } @@ -346,7 +520,7 @@ export class Translator { * @returns {string} */ _applyTextReplacements(text, sourceMap, replacements) { - for (const {pattern, replacement} of replacements) { + for (const { pattern, replacement } of replacements) { text = RegexUtil.applyTextReplacement(text, sourceMap, pattern, replacement); } return text; @@ -360,7 +534,7 @@ export class Translator { const jp = this._japaneseUtil; let length = 0; for (const c of text) { - if (!jp.isCodePointJapanese(/** @type {number} */ (c.codePointAt(0)))) { + if (!jp.isCodePointJapanese(/** @type {number} */(c.codePointAt(0)))) { return text.substring(0, length); } length += c.length; @@ -415,7 +589,7 @@ export class Translator { * @returns {import('translation-internal').DatabaseDeinflection} */ _createDeinflection(originalText, transformedText, deinflectedText, rules, reasons) { - return {originalText, transformedText, deinflectedText, rules, reasons, databaseEntries: []}; + return { originalText, transformedText, deinflectedText, rules, reasons, databaseEntries: [], isExtra: false }; } // Term dictionary entry grouping @@ -437,7 +611,7 @@ export class Translator { /** @type {Map} */ const ungroupedDictionaryEntriesMap = new Map(); for (const dictionaryEntry of dictionaryEntries) { - const {definitions: [{id, dictionary, sequences: [sequence]}]} = dictionaryEntry; + const { definitions: [{ id, dictionary, sequences: [sequence] }] } = dictionaryEntry; if (mainDictionary === dictionary && sequence >= 0) { let group = groupedDictionaryEntriesMap.get(sequence); if (typeof group === 'undefined') { @@ -445,7 +619,7 @@ export class Translator { ids: new Set(), dictionaryEntries: [] }; - sequenceList.push({query: sequence, dictionary}); + sequenceList.push({ query: sequence, dictionary }); groupedDictionaryEntries.push(group); groupedDictionaryEntriesMap.set(sequence, group); } @@ -485,11 +659,11 @@ export class Translator { 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]; - const {id} = databaseEntry; + const { dictionaryEntries, ids } = groupedDictionaryEntries[databaseEntry.index]; + const { id } = databaseEntry; if (ids.has(id)) { continue; } - const {term} = databaseEntry; + const { term } = databaseEntry; const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, term, term, term, [], false, enabledDictionaryMap, tagAggregator); dictionaryEntries.push(dictionaryEntry); ids.add(id); @@ -512,9 +686,9 @@ export class Translator { const targetMap = new Map(); for (const group of groupedDictionaryEntries) { - const {dictionaryEntries} = group; + const { dictionaryEntries } = group; for (const dictionaryEntry of dictionaryEntries) { - const {term, reading} = dictionaryEntry.headwords[0]; + const { term, reading } = dictionaryEntry.headwords[0]; const key = this._createMapKey([term, reading]); let target = targetMap.get(key); if (typeof target === 'undefined') { @@ -522,7 +696,7 @@ export class Translator { groups: [] }; targetMap.set(key, target); - termList.push({term, reading}); + termList.push({ term, reading }); targetList.push(target); } target.groups.push(group); @@ -531,12 +705,12 @@ export class Translator { // Group unsequenced dictionary entries with sequenced entries that have a matching [term, reading]. for (const [id, dictionaryEntry] of ungroupedDictionaryEntriesMap.entries()) { - const {term, reading} = dictionaryEntry.headwords[0]; + const { term, reading } = dictionaryEntry.headwords[0]; const key = this._createMapKey([term, reading]); const target = targetMap.get(key); if (typeof target === 'undefined') { continue; } - for (const {ids, dictionaryEntries} of target.groups) { + for (const { ids, dictionaryEntries } of target.groups) { if (ids.has(id)) { continue; } dictionaryEntries.push(dictionaryEntry); ids.add(id); @@ -551,10 +725,10 @@ export class Translator { this._sortDatabaseEntriesByIndex(databaseEntries); for (const databaseEntry of databaseEntries) { - const {index, id} = databaseEntry; + const { index, id } = databaseEntry; const sourceText = termList[index].term; const target = targetList[index]; - for (const {ids, dictionaryEntries} of target.groups) { + for (const { ids, dictionaryEntries } of target.groups) { if (ids.has(id)) { continue; } const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, sourceText, sourceText, sourceText, [], false, enabledDictionaryMap, tagAggregator); @@ -573,7 +747,7 @@ export class Translator { _groupDictionaryEntriesByHeadword(dictionaryEntries, tagAggregator) { const groups = new Map(); for (const dictionaryEntry of dictionaryEntries) { - const {inflections, headwords: [{term, reading}]} = dictionaryEntry; + const { inflections, headwords: [{ term, reading }] } = dictionaryEntry; const key = this._createMapKey([term, reading, ...inflections]); let groupDictionaryEntries = groups.get(key); if (typeof groupDictionaryEntries === 'undefined') { @@ -599,7 +773,7 @@ export class Translator { _removeExcludedDefinitions(dictionaryEntries, excludeDictionaryDefinitions) { for (let i = dictionaryEntries.length - 1; i >= 0; --i) { const dictionaryEntry = dictionaryEntries[i]; - const {definitions, pronunciations, frequencies, headwords} = dictionaryEntry; + const { definitions, pronunciations, frequencies, headwords } = dictionaryEntry; const definitionsChanged = this._removeArrayItemsWithDictionary(definitions, excludeDictionaryDefinitions); this._removeArrayItemsWithDictionary(pronunciations, excludeDictionaryDefinitions); this._removeArrayItemsWithDictionary(frequencies, excludeDictionaryDefinitions); @@ -620,12 +794,12 @@ export class Translator { * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry */ _removeUnusedHeadwords(dictionaryEntry) { - const {definitions, pronunciations, frequencies, headwords} = dictionaryEntry; + const { definitions, pronunciations, frequencies, headwords } = dictionaryEntry; const removeHeadwordIndices = new Set(); for (let i = 0, ii = headwords.length; i < ii; ++i) { removeHeadwordIndices.add(i); } - for (const {headwordIndices} of definitions) { + for (const { headwordIndices } of definitions) { for (const headwordIndex of headwordIndices) { removeHeadwordIndices.delete(headwordIndex); } @@ -657,7 +831,7 @@ export class Translator { * @param {Map} indexRemap */ _updateDefinitionHeadwordIndices(definitions, indexRemap) { - for (const {headwordIndices} of definitions) { + for (const { headwordIndices } of definitions) { for (let i = headwordIndices.length - 1; i >= 0; --i) { const newHeadwordIndex = indexRemap.get(headwordIndices[i]); if (typeof newHeadwordIndex === 'undefined') { @@ -676,7 +850,7 @@ export class Translator { _updateArrayItemsHeadwordIndex(array, indexRemap) { for (let i = array.length - 1; i >= 0; --i) { const item = array[i]; - const {headwordIndex} = item; + const { headwordIndex } = item; const newHeadwordIndex = indexRemap.get(headwordIndex); if (typeof newHeadwordIndex === 'undefined') { array.splice(i, 1); @@ -694,7 +868,7 @@ export class Translator { _removeArrayItemsWithDictionary(array, excludeDictionaryDefinitions) { let changed = false; for (let j = array.length - 1; j >= 0; --j) { - const {dictionary} = array[j]; + const { dictionary } = array[j]; if (!excludeDictionaryDefinitions.has(dictionary)) { continue; } array.splice(j, 1); changed = true; @@ -710,7 +884,7 @@ export class Translator { _removeArrayItemsWithDictionary2(array, excludeDictionaryDefinitions) { let changed = false; for (let j = array.length - 1; j >= 0; --j) { - const {dictionaries} = array[j]; + const { dictionaries } = array[j]; if (this._hasAny(excludeDictionaryDefinitions, dictionaries)) { continue; } array.splice(j, 1); changed = true; @@ -723,7 +897,7 @@ export class Translator { * @param {Set} excludeDictionaryDefinitions */ _removeTagGroupsWithDictionary(array, excludeDictionaryDefinitions) { - for (const {tags} of array) { + for (const { tags } of array) { this._removeArrayItemsWithDictionary2(tags, excludeDictionaryDefinitions); } } @@ -745,8 +919,8 @@ export class Translator { const allItems = []; /** @type {import('translator').TagTargetMap} */ const targetMap = new Map(); - for (const {tagGroups, tags} of tagTargets) { - for (const {dictionary, tagNames} of tagGroups) { + for (const { tagGroups, tags } of tagTargets) { + for (const { dictionary, tagNames } of tagGroups) { let dictionaryItems = targetMap.get(dictionary); if (typeof dictionaryItems === 'undefined') { dictionaryItems = new Map(); @@ -756,7 +930,7 @@ export class Translator { let item = dictionaryItems.get(tagName); if (typeof item === 'undefined') { const query = this._getNameBase(tagName); - item = {query, dictionary, tagName, cache: null, databaseTag: null, targets: []}; + item = { query, dictionary, tagName, cache: null, databaseTag: null, targets: [] }; dictionaryItems.set(tagName, item); allItems.push(item); } @@ -798,7 +972,7 @@ export class Translator { } } - for (const {dictionary, tagName, databaseTag, targets} of allItems) { + for (const { dictionary, tagName, databaseTag, targets } of allItems) { for (const tags of targets) { tags.push(this._createTag(databaseTag, tagName, dictionary)); } @@ -820,7 +994,7 @@ export class Translator { return i !== 0 ? i : stringComparer.compare(v1.name, v2.name); }; - for (const {tags} of tagTargets) { + for (const { tags } of tagTargets) { if (tags.length <= 1) { continue; } this._mergeSimilarTags(tags); tags.sort(compare); @@ -834,7 +1008,7 @@ export class Translator { let tagCount = tags.length; for (let i = 0; i < tagCount; ++i) { const tag1 = tags[i]; - const {category, name} = tag1; + const { category, name } = tag1; for (let j = i + 1; j < tagCount; ++j) { const tag2 = tags[j]; if (tag2.name !== name || tag2.category !== category) { continue; } @@ -875,7 +1049,7 @@ export class Translator { let lastPartOfSpeech = ''; const removeCategoriesSet = new Set(); - for (const {dictionary, tags} of definitions) { + for (const { dictionary, tags } of definitions) { const partOfSpeech = this._createMapKey(this._getTagNamesWithCategory(tags, 'partOfSpeech')); if (lastDictionary !== dictionary) { @@ -912,9 +1086,9 @@ export class Translator { const headwordMapKeys = []; const headwordReadingMaps = []; - for (const {headwords, pronunciations, frequencies} of dictionaryEntries) { + for (const { headwords, pronunciations, frequencies } of dictionaryEntries) { for (let i = 0, ii = headwords.length; i < ii; ++i) { - const {term, reading} = headwords[i]; + const { term, reading } = headwords[i]; let readingMap = headwordMap.get(term); if (typeof readingMap === 'undefined') { readingMap = new Map(); @@ -927,13 +1101,13 @@ export class Translator { targets = []; readingMap.set(reading, targets); } - targets.push({headwordIndex: i, pronunciations, frequencies}); + targets.push({ headwordIndex: i, pronunciations, frequencies }); } } const metas = await this._database.findTermMetaBulk(headwordMapKeys, enabledDictionaryMap); - for (const {mode, data, dictionary, index} of metas) { - const {index: dictionaryIndex, priority: dictionaryPriority} = this._getDictionaryOrder(dictionary, enabledDictionaryMap); + for (const { mode, data, dictionary, index } of metas) { + const { index: dictionaryIndex, priority: dictionaryPriority } = this._getDictionaryOrder(dictionary, enabledDictionaryMap); const map2 = headwordReadingMaps[index]; for (const [reading, targets] of map2.entries()) { switch (mode) { @@ -942,8 +1116,8 @@ export class Translator { const hasReading = (data !== null && typeof data === 'object' && typeof data.reading === 'string'); if (hasReading && data.reading !== reading) { continue; } const frequency = hasReading ? data.frequency : /** @type {import('dictionary-data').GenericFrequencyData} */ (data); - for (const {frequencies, headwordIndex} of targets) { - const {frequency: frequencyValue, displayValue, displayValueParsed} = this._getFrequencyInfo(frequency); + for (const { frequencies, headwordIndex } of targets) { + const { frequency: frequencyValue, displayValue, displayValueParsed } = this._getFrequencyInfo(frequency); frequencies.push(this._createTermFrequency( frequencies.length, headwordIndex, @@ -963,7 +1137,7 @@ export class Translator { if (data.reading !== reading) { continue; } /** @type {import('dictionary').TermPitch[]} */ const pitches = []; - for (const {position, tags, nasal, devoice} of data.pitches) { + for (const { position, tags, nasal, devoice } of data.pitches) { /** @type {import('dictionary').Tag[]} */ const tags2 = []; if (Array.isArray(tags)) { @@ -971,9 +1145,9 @@ export class Translator { } const nasalPositions = this._toNumberArray(nasal); const devoicePositions = this._toNumberArray(devoice); - pitches.push({position, nasalPositions, devoicePositions, tags: tags2}); + pitches.push({ position, nasalPositions, devoicePositions, tags: tags2 }); } - for (const {pronunciations, headwordIndex} of targets) { + for (const { pronunciations, headwordIndex } of targets) { pronunciations.push(this._createTermPronunciation( pronunciations.length, headwordIndex, @@ -996,18 +1170,18 @@ export class Translator { */ async _addKanjiMeta(dictionaryEntries, enabledDictionaryMap) { const kanjiList = []; - for (const {character} of dictionaryEntries) { + for (const { character } of dictionaryEntries) { kanjiList.push(character); } const metas = await this._database.findKanjiMetaBulk(kanjiList, enabledDictionaryMap); - for (const {character, mode, data, dictionary, index} of metas) { - const {index: dictionaryIndex, priority: dictionaryPriority} = this._getDictionaryOrder(dictionary, enabledDictionaryMap); + for (const { character, mode, data, dictionary, index } of metas) { + const { index: dictionaryIndex, priority: dictionaryPriority } = this._getDictionaryOrder(dictionary, enabledDictionaryMap); switch (mode) { case 'freq': { - const {frequencies} = dictionaryEntries[index]; - const {frequency, displayValue, displayValueParsed} = this._getFrequencyInfo(data); + const { frequencies } = dictionaryEntries[index]; + const { frequency, displayValue, displayValueParsed } = this._getFrequencyInfo(data); frequencies.push(this._createKanjiFrequency( frequencies.length, dictionary, @@ -1034,7 +1208,7 @@ export class Translator { const items = []; for (const [name] of statsEntries) { const query = this._getNameBase(name); - items.push({query, dictionary}); + items.push({ query, dictionary }); } const databaseInfos = await this._database.findTagMetaBulk(items); @@ -1046,7 +1220,7 @@ export class Translator { if (typeof databaseInfo === 'undefined') { continue; } const [name, value] = statsEntries[i]; - const {category} = databaseInfo; + const { category } = databaseInfo; let group = statsGroups.get(category); if (typeof group === 'undefined') { group = []; @@ -1097,7 +1271,7 @@ export class Translator { let displayValue = null; let displayValueParsed = false; if (typeof frequency === 'object' && frequency !== null) { - const {value: frequencyValue2, displayValue: displayValue2} = frequency; + const { value: frequencyValue2, displayValue: displayValue2 } = frequency; if (typeof frequencyValue2 === 'number') { frequencyValue = frequencyValue2; } if (typeof displayValue2 === 'string') { displayValue = displayValue2; } } else { @@ -1112,7 +1286,7 @@ export class Translator { break; } } - return {frequency: frequencyValue, displayValue, displayValueParsed}; + return { frequency: frequencyValue, displayValue, displayValueParsed }; } // Helpers @@ -1146,8 +1320,8 @@ export class Translator { */ _getDictionaryOrder(dictionary, enabledDictionaryMap) { const info = enabledDictionaryMap.get(dictionary); - const {index, priority} = typeof info !== 'undefined' ? info : {index: enabledDictionaryMap.size, priority: 0}; - return {index, priority}; + const { index, priority } = typeof info !== 'undefined' ? info : { index: enabledDictionaryMap.size, priority: 0 }; + return { index, priority }; } /** @@ -1201,7 +1375,7 @@ export class Translator { * @returns {import('dictionary').KanjiStat} */ _createKanjiStat(name, value, databaseInfo, dictionary) { - const {category, notes, order, score} = databaseInfo; + const { category, notes, order, score } = databaseInfo; return { name, category: (typeof category === 'string' && category.length > 0 ? category : 'default'), @@ -1225,7 +1399,7 @@ export class Translator { * @returns {import('dictionary').KanjiFrequency} */ _createKanjiFrequency(index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue, displayValueParsed) { - return {index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue, displayValueParsed}; + return { index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue, displayValueParsed }; } /** @@ -1262,7 +1436,7 @@ export class Translator { _createTag(databaseTag, name, dictionary) { let category, notes, order, score; if (typeof databaseTag === 'object' && databaseTag !== null) { - ({category, notes, order, score} = databaseTag); + ({ category, notes, order, score } = databaseTag); } return { name, @@ -1285,7 +1459,7 @@ export class Translator { * @returns {import('dictionary').TermSource} */ _createSource(originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary) { - return {originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary}; + return { originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary }; } /** @@ -1298,7 +1472,7 @@ export class Translator { * @returns {import('dictionary').TermHeadword} */ _createTermHeadword(index, term, reading, sources, tags, wordClasses) { - return {index, term, reading, sources, tags, wordClasses}; + return { index, term, reading, sources, tags, wordClasses }; } /** @@ -1342,7 +1516,7 @@ export class Translator { * @returns {import('dictionary').TermPronunciation} */ _createTermPronunciation(index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches) { - return {index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches}; + return { index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches }; } /** @@ -1358,7 +1532,7 @@ export class Translator { * @returns {import('dictionary').TermFrequency} */ _createTermFrequency(index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue, displayValueParsed) { - return {index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue, displayValueParsed}; + return { index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue, displayValueParsed }; } /** @@ -1403,9 +1577,9 @@ export class Translator { * @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 { 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); + const { index: dictionaryIndex, priority: dictionaryPriority } = this._getDictionaryOrder(dictionary, enabledDictionaryMap); const sourceTermExactMatchCount = (isPrimary && deinflectedText === term ? 1 : 0); const source = this._createSource(originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary); const maxTransformedTextLength = transformedText.length; @@ -1445,7 +1619,7 @@ export class Translator { const headwords = new Map(); for (const dictionaryEntry of dictionaryEntries) { const headwordIndexMap = this._addTermHeadwords(headwords, dictionaryEntry.headwords, tagAggregator); - definitionEntries.push({index: definitionEntries.length, dictionaryEntry, headwordIndexMap}); + definitionEntries.push({ index: definitionEntries.length, dictionaryEntry, headwordIndexMap }); } // Sort @@ -1465,7 +1639,7 @@ export class Translator { const definitionsMap = checkDuplicateDefinitions ? new Map() : null; let inflections = null; - for (const {dictionaryEntry, headwordIndexMap} of definitionEntries) { + for (const { dictionaryEntry, headwordIndexMap } of definitionEntries) { score = Math.max(score, dictionaryEntry.score); dictionaryIndex = Math.min(dictionaryIndex, dictionaryEntry.dictionaryIndex); dictionaryPriority = Math.max(dictionaryPriority, dictionaryEntry.dictionaryPriority); @@ -1487,7 +1661,7 @@ export class Translator { const headwordsArray = [...headwords.values()]; let sourceTermExactMatchCount = 0; - for (const {sources} of headwordsArray) { + for (const { sources } of headwordsArray) { for (const source of sources) { if (source.isPrimary && source.matchSource === 'term') { ++sourceTermExactMatchCount; @@ -1535,7 +1709,7 @@ export class Translator { return; } for (const newSource of newSources) { - const {originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary} = newSource; + const { originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary } = newSource; let has = false; for (const source of sources) { if ( @@ -1565,7 +1739,7 @@ export class Translator { _addTermHeadwords(headwordsMap, headwords, tagAggregator) { /** @type {number[]} */ const headwordIndexMap = []; - for (const {term, reading, sources, tags, wordClasses} of headwords) { + for (const { term, reading, sources, tags, wordClasses } of headwords) { const key = this._createMapKey([term, reading]); let headword = headwordsMap.get(key); if (typeof headword === 'undefined') { @@ -1613,7 +1787,7 @@ export class Translator { * @param {number[]} headwordIndexMap */ _addTermDefinitionsFast(definitions, newDefinitions, headwordIndexMap) { - for (const {headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, sequences, id, score, isPrimary, tags, entries} of newDefinitions) { + for (const { headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, sequences, id, score, isPrimary, tags, entries } of newDefinitions) { const headwordIndicesNew = []; for (const headwordIndex of headwordIndices) { headwordIndicesNew.push(headwordIndexMap[headwordIndex]); @@ -1630,7 +1804,7 @@ export class Translator { * @param {TranslatorTagAggregator} tagAggregator */ _addTermDefinitions(definitions, definitionsMap, newDefinitions, headwordIndexMap, tagAggregator) { - for (const {headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, sequences, id, score, isPrimary, tags, entries} of newDefinitions) { + 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); if (typeof definition === 'undefined') { @@ -1832,7 +2006,7 @@ export class Translator { return i; }; - for (const {frequencies} of dictionaryEntries) { + for (const { frequencies } of dictionaryEntries) { frequencies.sort(compare); } } @@ -1845,12 +2019,12 @@ export class Translator { _updateSortFrequencies(dictionaryEntries, dictionary, ascending) { const frequencyMap = new Map(); for (const dictionaryEntry of dictionaryEntries) { - const {definitions, frequencies} = dictionaryEntry; + const { definitions, frequencies } = dictionaryEntry; let frequencyMin = Number.MAX_SAFE_INTEGER; let frequencyMax = Number.MIN_SAFE_INTEGER; for (const item of frequencies) { if (item.dictionary !== dictionary) { continue; } - const {headwordIndex, frequency} = item; + const { headwordIndex, frequency } = item; if (typeof frequency !== 'number') { continue; } frequencyMap.set(headwordIndex, frequency); frequencyMin = Math.min(frequencyMin, frequency); @@ -1858,13 +2032,13 @@ export class Translator { } dictionaryEntry.frequencyOrder = ( frequencyMin <= frequencyMax ? - (ascending ? frequencyMin : -frequencyMax) : - (ascending ? Number.MAX_SAFE_INTEGER : 0) + (ascending ? frequencyMin : -frequencyMax) : + (ascending ? Number.MAX_SAFE_INTEGER : 0) ); for (const definition of definitions) { frequencyMin = Number.MAX_SAFE_INTEGER; frequencyMax = Number.MIN_SAFE_INTEGER; - const {headwordIndices} = definition; + const { headwordIndices } = definition; for (const headwordIndex of headwordIndices) { const frequency = frequencyMap.get(headwordIndex); if (typeof frequency !== 'number') { continue; } @@ -1873,8 +2047,8 @@ export class Translator { } definition.frequencyOrder = ( frequencyMin <= frequencyMax ? - (ascending ? frequencyMin : -frequencyMax) : - (ascending ? Number.MAX_SAFE_INTEGER : 0) + (ascending ? frequencyMin : -frequencyMax) : + (ascending ? Number.MAX_SAFE_INTEGER : 0) ); } frequencyMap.clear(); @@ -1921,7 +2095,7 @@ class TranslatorTagAggregator { getTagExpansionTargets() { const results = []; for (const [tags, tagGroups] of this._tagExpansionTargetMap) { - results.push({tags, tagGroups}); + results.push({ tags, tagGroups }); } return results; } @@ -1934,7 +2108,7 @@ class TranslatorTagAggregator { const newTagGroups = this._tagExpansionTargetMap.get(newTags); if (typeof newTagGroups === 'undefined') { return; } const tagGroups = this._getOrCreateTagGroups(tags); - for (const {dictionary, tagNames} of newTagGroups) { + for (const { dictionary, tagNames } of newTagGroups) { const tagGroup = this._getOrCreateTagGroup(tagGroups, dictionary); this._addUniqueTags(tagGroup, tagNames); } @@ -1962,7 +2136,7 @@ class TranslatorTagAggregator { for (const tagGroup of tagGroups) { if (tagGroup.dictionary === dictionary) { return tagGroup; } } - const newTagGroup = {dictionary, tagNames: []}; + const newTagGroup = { dictionary, tagNames: [] }; tagGroups.push(newTagGroup); return newTagGroup; } @@ -1972,7 +2146,7 @@ class TranslatorTagAggregator { * @param {string[]} newTagNames */ _addUniqueTags(tagGroup, newTagNames) { - const {tagNames} = tagGroup; + const { tagNames } = tagGroup; for (const tagName of newTagNames) { if (tagNames.includes(tagName)) { continue; } tagNames.push(tagName); diff --git a/ext/search.html b/ext/search.html index 8c595cc4e3..e4ad7d978d 100644 --- a/ext/search.html +++ b/ext/search.html @@ -49,7 +49,8 @@

Yomitan Search

- + +
diff --git a/types/ext/dictionary-database.d.ts b/types/ext/dictionary-database.d.ts index 6569f76bf5..1796f957a7 100644 --- a/types/ext/dictionary-database.d.ts +++ b/types/ext/dictionary-database.d.ts @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * * This program is free software: you can redistribute it and/or modify @@ -57,6 +58,7 @@ export type DatabaseTermEntry = { export type DatabaseTermEntryWithId = DatabaseTermEntry & DatabaseId; export type TermEntry = { + skip: boolean; index: number; matchType: MatchType; matchSource: MatchSource; diff --git a/types/ext/translation-internal.d.ts b/types/ext/translation-internal.d.ts index 784a597983..d6cd32b840 100644 --- a/types/ext/translation-internal.d.ts +++ b/types/ext/translation-internal.d.ts @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * * This program is free software: you can redistribute it and/or modify @@ -62,4 +63,5 @@ export type DatabaseDeinflection = { rules: DeinflectionRuleFlags; reasons: string[]; databaseEntries: DictionaryDatabase.TermEntry[]; + isExtra: boolean; }; From 19430c7cdbe2d268775e993e47c979582f603c0a Mon Sep 17 00:00:00 2001 From: Cashew Date: Sun, 10 Dec 2023 11:10:53 +0900 Subject: [PATCH 02/12] update README.md --- README.md | 422 +---------------------------------- ext/js/background/backend.js | 4 + 2 files changed, 10 insertions(+), 416 deletions(-) diff --git a/README.md b/README.md index 3fbf67c84d..53427e0a85 100644 --- a/README.md +++ b/README.md @@ -1,425 +1,15 @@ -# Yomitan - -[![Chrome Release (Stable)]()](https://chrome.google.com/webstore/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn) [![Firefox Release (Stable)]()](https://addons.mozilla.org/en-US/firefox/addon/yomitan/) -[![Chrome Release (Testing)]()](https://chrome.google.com/webstore/detail/yomitan-development-build/glnaenfapkkecknnmginabpmgkenenml) [![Firefox Release (Testing)]()](https://github.com/themoeway/yomitan/releases) -[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/themoeway/yomitan/badge)](https://securityscorecards.dev/viewer/?uri=github.com/themoeway/yomitan) [![Discord Server](https://dcbadge.vercel.app/api/server/UGNPMDE7zC?style=flat)](https://discord.gg/UGNPMDE7zC) +# Lesen-tan ## Project Introduction -:wave: **This project is a community fork of Yomichan** (which was [sunset](https://foosoft.net/posts/sunsetting-the-yomichan-project/) by its owner on Feb 26 2023). - -We have made a number of foundational changes to ensure **the project stays alive, works on latest browser versions, and is easy to contribute to**: -* Completed the Manifest V2 → Manifest V3 transition, which is required to submit a new extension to the Chrome webstore. It will also be long-term required for usage of the extension, as [Manifest V2 extensions will start being disabled as early as June 2024](https://developer.chrome.com/blog/resuming-the-transition-to-mv3/). -* Switched to using ECMAScript modules and npm-sourced dependencies to make for a more modern coding and packaging experience. -* Implemented an end-to-end CI/CD pipeline to make it easy to rapidly iterate and deploy new versions. -* Switched to standard testing frameworks, vitest and playwright, to make it easier to develop more comprehensive tests, and detect regressions. - -In addition, we are beginning to make important bug fixes and minor enhancements: -* Improve dictionary import speed by 2x~10x or more (depending on the dictionary) -* Fix UI regressions on modern browser versions, like [the popup being too small](https://github.com/themoeway/yomitan/pull/228) -* Add functionality to import/export multiple dictionaries, to make your data more portable across machines -* And [more](https://github.com/themoeway/yomitan/pulls?q=is%3Apr+is%3Amerged+-label%3Aarea%2Fdependencies+-label%3Akind%2Fmeta) - -Since the owner requested forks be uniquely named, we have chosen a new name, _yomitan_. (_-tan_ is an honorific used for anthropomorphic moe characters.) While we've made some substantial changes, the majority of the extension's functionality is thanks to hard work of foosoft and numerous other open source contributors from 2016-2023. - -Since this is a distributed effort, we **highly welcome new contributors**! Feel free to browse the issue tracker, and you can find us on [TheMoeWay Discord](https://discord.gg/UGNPMDE7zC) at [#yomitan-development](https://discord.com/channels/617136488840429598/1081538711742844980). - -## Tool Introduction -Yomitan turns your web browser into a tool for building Japanese language literacy by helping you to decipher texts -which would be otherwise too difficult tackle. This extension is similar to -[10ten Japanese Reader (formerly Rikaichamp)](https://addons.mozilla.org/en-US/firefox/addon/10ten-ja-reader/) for Firefox and -[Rikaikun](https://chrome.google.com/webstore/detail/rikaikun/jipdnfibhldikgcjhfnomkfpcebammhp?hl=en) for Chrome, but it -stands apart in its goal of being an all-encompassing learning tool as opposed to a mere browser-based dictionary. - -Yomitan provides advanced features not available in other browser-based dictionaries: +:wave: **A hover dictionary for conjugated languages (German, French, Spanish)** -- Interactive popup definition window for displaying search results. -- On-demand audio playback for select dictionary definitions. -- Kanji stroke order diagrams are just a click away for most characters. -- Custom search page for easily executing custom search queries. -- Support for multiple dictionary formats including [EPWING](https://ja.wikipedia.org/wiki/EPWING) via the [Yomitan Import](https://github.com/themoeway/yomitan-import) tool. -- Automatic note creation for the [Anki](https://apps.ankiweb.net/) flashcard program via the [AnkiConnect](https://foosoft.net/projects/anki-connect) plugin. -- Clean, modern code makes it easy for developers to [contribute](https://github.com/themoeway/yomitan/blob/master/CONTRIBUTING.md) new features. +This is a fork of the amazing [Yomitan](https://github.com/themoeway/yomitan) project for Japanese, and is based on [yomichan-de](https://github.com/seth-js/yomichan-de). Please check out their github repos for more detailed information about the projects themselves. -[![Term definitions](img/ss-terms-thumb.png)](img/ss-terms.png) -[![Kanji information](img/ss-kanji-thumb.png)](img/ss-kanji.png) -[![Dictionary options](img/ss-dictionaries-thumb.png)](img/ss-dictionaries.png) -[![Anki options](img/ss-anki-thumb.png)](img/ss-anki.png) +If you find any issues or simply want to contribute, please open an issue via the [issue tracker](https://github.com/Scrub1492/lesen-tan/issues) or find my on Discord under the name cashewnuttynuts. -## Table of Contents - -- [Installation](#installation) -- [Migrating from Yomichan](#migrating-from-yomichan) - - [Exporting Data](#exporting-data) - - [Custom Templates](#custom-templates) -- [Dictionaries](#dictionaries) -- [Basic Usage](#basic-usage) - - [Importing Dictionaries](#importing-dictionaries) - - [Importing and Exporting Personal Configuration](#importing-and-exporting-personal-configuration) -- [Custom Dictionaries](#custom-dictionaries) -- [Anki Integration](#anki-integration) - - [Flashcard Configuration](#flashcard-configuration) - - [Flashcard Creation](#flashcard-creation) -- [Keyboard Shortcuts](#keyboard-shortcuts) -- [Advanced Options](#advanced-options) - - [Parse sentences using MeCab](#parse-sentences-using-mecab) -- [Frequently Asked Questions](#frequently-asked-questions) -- [Licenses](#licenses) -- [Third-Party Libraries](#third-party-libraries) +**Currently Lesen-tan will work with only German. It will currently have unintended results when performing look-ups in other languages (including Japanese). ## Installation -Yomitan comes in two flavors: _stable_ and _testing_. Over the years, this extension has evolved to contain many -complex features which have become increasingly difficult to test across different browsers, versions, and environments. -New changes are initially introduced into the _testing_ version, and after some time spent ensuring that they are -relatively bug free, they will be promoted to the _stable_ version. If you are technically savvy and don't mind -submitting issues on GitHub, try the _testing_ version; otherwise, the _stable_ version will be your best bet. - -- **Google Chrome** - - - [stable](https://chrome.google.com/webstore/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn) - - [testing](https://chrome.google.com/webstore/detail/yomitan-development-build/glnaenfapkkecknnmginabpmgkenenml) - -- **Mozilla Firefox** - - [stable](https://addons.mozilla.org/en-US/firefox/addon/yomitan/) - - [testing](https://github.com/themoeway/yomitan/releases) ※ - -※ NOTE: Unlike Chrome, Firefox does not allow extensions meant for testing to be hosted in the marketplace. -You will have to download a desired version and side-load it yourself. You only need to do this once and will get -updates automatically. - -## Migrating from Yomichan - -### Exporting Data - -If you are an existing user of Yomichan, you can export your dictionary collection and settings such that they can be imported into Yomitan to reflect your setup exactly as it was. - -You can export your settings from Yomichan's Settings page. Go to the `Backup` section and click on `Export Settings`. - -Yomichan doesn't have first-class support to export the dictionary collection. Please follow the instructions provided in the following link to export your data: -https://github.com/themoeway/yomichan-data-exporter#steps-to-export-the-data - -You can then import the exported files into Yomitan from the `Backup` section of the `Settings` page. Please see [the section on importing dictionaries](#importing-dictionaries) further below for more explicit steps. - -### Custom Templates - -If you do not use custom templates for Anki note creation, this section can be skipped. - -Due to security concerns, an alternate implementation of Handlebars is being used which behaves slightly differently. -This revealed a bug in four of Yomitan's template helpers, which have now been fixed in the default templates. If your -custom templates use the following helpers, please ensure their use matches the corrected forms. - -| Helper | Example | Corrected | -| ---------------- | ------------------------------------------------------------- | ------------------------------------ | -| `formatGlossary` | `{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}` | `{{formatGlossary ../dictionary .}}` | -| `furigana` | `{{#furigana}}{{{definition}}}{{/furigana}}` | `{{furigana definition}}` | -| `furiganaPlain` | `{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}` | `{{~furiganaPlain .~}}` | -| `dumpObject` | `{{#dumpObject}}{{{.}}}{{/dumpObject}}` | `{{dumpObject .}}` | - -Authors of custom templates may be interested to know that other helpers previously used and documented in the block -form (e.g. `{{#set "key" "value"}}{{/set}}`), while not broken by this change, may also be replaced with the less verbose -form (e.g. `{{set "key" "value"}}`). The default templates and helper documentation have been changed to reflect this. - -## Dictionaries - -There are several free Japanese dictionaries available for Yomitan, with two of them having glossaries available in -different languages. You must download and import the dictionaries you wish to use in order to enable Yomitan -definition lookups. If you have proprietary EPWING dictionaries that you would like to use, check the [Yomitan -Import](https://github.com/themoeway/yomitan-import) page to learn how to convert and import them into Yomitan. - -Be aware that non-English dictionaries contain fewer entries than their English counterparts. Even if your primary -language is not English, you may consider also importing the English version for better coverage. - -- [Jitendex](https://github.com/stephenmk/Jitendex) - Jitendex is an improved version of JMdict for Yomitan. It features better formatting and some other improvements, and is actively being improved by its author. -- [JMdict](https://github.com/themoeway/jmdict-yomitan#jmdict-for-yomitan-1) - There are daily automatically updated builds of JMdict for Yomitan available in this repository. It is available in multiple languages and formats, but we recommend installing the more modern Jitendex for English users. -- [JMnedict](https://github.com/themoeway/jmdict-yomitan#jmnedict-for-yomitan) - JMnedict is a dictionary that lists readings of person/place/organization names and other proper nouns. -- [KANJIDIC](https://github.com/themoeway/jmdict-yomitan#kanjidic-for-yomitan) - KANJIDIC is an English dictionary listing readings, meanings, and other info about kanji characters. - -## Basic Usage - -1. Click the _Yomitan_ button in the browser bar to open the quick-actions popup. - - - - - The _cog_ button will open the Settings page. - - The _magnifying glass_ button will open the Search page. - - The _question mark_ button will open the Information page. - - The _profile_ button will appear when multiple profiles exist, allowing the current profile to be quickly changed. - -2. Import the dictionaries you wish to use for term and kanji searches. If you do not have any dictionaries installed - or enabled, Yomitan will warn you that it is not ready for use by displaying an orange exclamation mark over its - icon. This exclamation mark will disappear once you have installed and enabled at least one dictionary. - - - -3. Webpage text can be scanned by moving the cursor while holding a modifier key, which is Shift - by default. If definitions are found for the text at the cursor position, a popup window containing term definitions - will open. This window can be dismissed by clicking anywhere outside of it. - - - -4. Click on the _speaker_ button to hear the term pronounced by a native speaker. If an audio sample is - not available, you will hear a short click instead. You can configure the sources used to retrieve audio samples in - the options page. - -5. Click on individual kanji in the term definition results to view additional information about those characters, - including stroke order diagrams, readings, meanings, as well as other useful data. - - - -### Importing Dictionaries - -You can import individual dictionaries from the settings page as described above. - -Yomitan also supports exporting and importing your entire collection of dictionaries. - -#### Importing a Dictionary Collection - -- Go to Yomitan's Settings page (Click on the extension's icon then click on the cog icon from the popup) -- Click `Import Dictionary Collection` and select the database file you want to import -- Wait for the import to finish then turn all the dictionaries back on from the `Dictionaries > Configure installed and enabled dictionaries` section -- Refresh the browser tab to see the dictionaries in effect - -#### Exporting the Dictionary Collection - -- Click `Export Dictionary Collection` from the backup section of Yomitan's settings page -- It will show you a progress report as it exports the data then initiates a - download for a file named something like `yomitan-dictionaries-YYYY-MM-DD-HH-mm-ss.json` - (e.g. `yomitan-dictionaries-2023-07-05-02-42-04.json`) - -### Importing and Exporting Personal Configuration - -Note that you can also similarly export and import your Yomitan settings from the `Backup` section of the Settings page. - -You should be able to replicate your exact Yomitan setup across devices by exporting your settings and dictionary collection from the source device then importing those from the destination. - -## Custom Dictionaries - -Yomitan supports the use of custom dictionaries, including the esoteric but popular -[EPWING](https://ja.wikipedia.org/wiki/EPWING) format. They were often utilized in portable electronic dictionaries -similar to the ones pictured below. These dictionaries are often sought after by language learners for their correctness -and excellent coverage of the Japanese language. - -Unfortunately, as most of the dictionaries released in this format are proprietary, they are unable to be bundled with -Yomitan. Instead, you will need to procure these dictionaries yourself and import them using [Yomitan -Import](https://github.com/themoeway/yomitan-import). Check the project page for additional details. - -![Pocket EPWING dictionaries](img/epwing-devices.jpg) - -## Anki Integration - -Yomitan features automatic flashcard creation for [Anki](https://apps.ankiweb.net/), a free application designed to help you -retain knowledge. This feature requires the prior installation of an Anki plugin called [AnkiConnect](https://foosoft.net/projects/anki-connect). -Check the respective project page for more information about how to set up this software. - -### Flashcard Configuration - -Before flashcards can be automatically created, you must configure the templates used to create term and/or kanji notes. -If you are unfamiliar with Anki deck and model management, this would be a good time to reference the [Anki -Manual](https://docs.ankiweb.net/#/). In short, you must specify what information should be included in the -flashcards that Yomitan creates through AnkiConnect. - -Flashcard fields can be configured with the following steps: - -1. Open the Yomitan options page and scroll down to the section labeled _Anki Options_. -2. Tick the checkbox labeled _Enable Anki integration_ (Anki must be running with [AnkiConnect](https://foosoft.net/projects/anki-connect) installed). -3. Select the type of template to configure by clicking on either the _Terms_ or _Kanji_ tabs. -4. Select the Anki deck and model to use for new creating new flashcards of this type. -5. Fill the model fields with markers corresponding to the information you wish to include (several can be used at - once). Advanced users can also configure the actual [Handlebars](https://handlebarsjs.com/) templates used to create - the flashcard contents (this is strictly optional). - - #### Markers for Term Cards - - | Marker | Description | - | -------------------------- | ------------------------------------------------------------------------------------------------------------------------ | - | `{audio}` | Audio sample of a native speaker's pronunciation in MP3 format (if available). | - | `{clipboard-image}` | An image which is stored in the system clipboard, if present. | - | `{clipboard-text}` | Text which is stored in the system clipboard, if present. | - | `{cloze-body}` | Raw, inflected term as it appeared before being reduced to dictionary form by Yomitan. | - | `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. | - | `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. | - | `{conjugation}` | Conjugation path from the raw inflected term to the source term. | - | `{dictionary}` | Name of the dictionary from which the card is being created (unavailable in _grouped_ mode). | - | `{document-title}` | Title of the web page that the term appeared in. | - | `{expression}` | Term expressed as kanji (will be displayed in kana if kanji is not available). | - | `{frequencies}` | Frequency information for the term. | - | `{furigana}` | Term expressed as kanji with furigana displayed above it (e.g. 日本語にほんご). | - | `{furigana-plain}` | Term expressed as kanji with furigana displayed next to it in brackets (e.g. 日本語[にほんご]). | - | `{glossary}` | List of definitions for the term (output format depends on whether running in _grouped_ mode). | - | `{glossary-brief}` | List of definitions for the term in a more compact format. | - | `{glossary-no-dictionary}` | List of definitions for the term, except the dictionary tag is omitted. | - | `{part-of-speech}` | Part of speech information for the term. | - | `{pitch-accents}` | List of pitch accent downstep notations for the term. | - | `{pitch-accent-graphs}` | List of pitch accent graphs for the term. | - | `{pitch-accent-positions}` | List of accent downstep positions for the term as a number. | - | `{reading}` | Kana reading for the term (empty for terms where the expression is the reading). | - | `{screenshot}` | Screenshot of the web page taken at the time the term was added. | - | `{search-query}` | The full search query shown on the search page. | - | `{selection-text}` | The selected text on the search page or popup. | - | `{sentence}` | Sentence, quote, or phrase that the term appears in from the source content. | - | `{sentence-furigana}` | Sentence, quote, or phrase that the term appears in from the source content, with furigana added. | - | `{tags}` | Grammar and usage tags providing information about the term (unavailable in _grouped_ mode). | - | `{url}` | Address of the web page in which the term appeared in. | - - #### Markers for Kanji Cards - - | Marker | Description | - | --------------------- | ------------------------------------------------------------------------------------------------------------------------ | - | `{character}` | Unicode glyph representing the current kanji. | - | `{clipboard-image}` | An image which is stored in the system clipboard, if present. | - | `{clipboard-text}` | Text which is stored in the system clipboard, if present. | - | `{cloze-body}` | Raw, inflected parent term as it appeared before being reduced to dictionary form by Yomitan. | - | `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. | - | `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. | - | `{dictionary}` | Name of the dictionary from which the card is being created. | - | `{document-title}` | Title of the web page that the kanji appeared in. | - | `{frequencies}` | Frequency information for the kanji. | - | `{glossary}` | List of definitions for the kanji. | - | `{kunyomi}` | Kunyomi (Japanese reading) for the kanji expressed as katakana. | - | `{onyomi}` | Onyomi (Chinese reading) for the kanji expressed as hiragana. | - | `{screenshot}` | Screenshot of the web page taken at the time the kanji was added. | - | `{search-query}` | The full search query shown on the search page. | - | `{selection-text}` | The selected text on the search page or popup. | - | `{sentence}` | Sentence, quote, or phrase that the character appears in from the source content. | - | `{sentence-furigana}` | Sentence, quote, or phrase that the character appears in from the source content, with furigana added. | - | `{stroke-count}` | Number of strokes that the kanji character has. | - | `{url}` | Address of the web page in which the kanji appeared in. | - -When creating your model for Yomitan, _make sure that you pick a unique field to be first_; fields that will -contain `{expression}` or `{character}` are ideal candidates for this. Anki does not allow duplicate flashcards to be -added to a deck by default; it uses the first field in the model to check for duplicates. For example, if you have `{reading}` -configured to be the first field in your model and はし is already in your deck, you will not -be able to create a flashcard for はし because they share the same reading. - -### Flashcard Creation - -Once Yomitan is configured, it becomes trivial to create new flashcards with a single click. You will see the following -icons next to term definitions: - -- Clicking ![](img/btn-add-expression.png) adds the current expression as kanji (e.g. 食べる). -- Clicking ![](img/btn-add-reading.png) adds the current expression as hiragana or katakana (e.g. たべる). - -Below are some troubleshooting tips you can try if you are unable to create new flashcards: - -- Individual icons will appear grayed out if a flashcard cannot be created for the current definition (e.g. it already exists in the deck). -- If all of the buttons appear grayed out, then you should double-check your deck and model configuration settings. -- If no icons appear at all, make sure that Anki is running in the background and that [AnkiConnect](https://foosoft.net/projects/anki-connect) has been installed. - -## Keyboard Shortcuts - -The following shortcuts are globally available: - -| Shortcut | Action | -| ---------------------------------- | ------------------------ | -| Alt + Insert | Open search page. | -| Alt + Delete | Toggle extension on/off. | - -The following shortcuts are available on search results: - -| Shortcut | Action | -| -------------------------------- | --------------------------------------- | -| Esc | Cancel current search. | -| Alt + PgUp | Page up through results. | -| Alt + PgDn | Page down through results. | -| Alt + End | Go to last result. | -| Alt + Home | Go to first result. | -| Alt + Up | Go to previous result. | -| Alt + Down | Go to next result. | -| Alt + b | Go to back to source term. | -| Alt + e | Add current term as expression to Anki. | -| Alt + r | Add current term as reading to Anki. | -| Alt + p | Play audio for current term. | -| Alt + k | Add current kanji to Anki. | - -## Advanced Options - -Click the `Advanced` toggle switch in the bottom left corner of the Settings page to enable advanced options. - -### Parse sentences using MeCab - -[MeCab](https://taku910.github.io/mecab/) is a third-party program which uses its own dictionaries and parsing algorithm to decompose sentences into individual words. MeCab may provide more accurate parsing results than Yomitan's internal parser. - -In order for Yomitan to use it, both MeCab and a native messaging component must be installed. -A setup guide can be found [here](https://github.com/themoeway/yomitan-mecab-installer/blob/master/README.md). - -## Frequently Asked Questions - -**I'm having problems importing dictionaries in Firefox, what do I do?** - -Yomitan uses the cross-browser IndexedDB system for storing imported dictionary data into your user profile. Although -everything "just works" in Chrome, depending on settings, Firefox users can run into problems due to browser bugs. -Yomitan catches errors and tries to offer suggestions about how to work around Firefox issues, but in general at least -one of the following solutions should work for you: - -- Make sure you have cookies enabled. It appears that disabling them also disables IndexedDB for some reason. You - can still have cookies be disabled on other sites; just make sure to add the Yomitan extension to the whitelist of - whatever tool you are using to restrict cookies. You can get the extension "URL" by looking at the address bar when - you have the search page open. -- Make sure that you have sufficient disk space available on the drive Firefox uses to store your user profile. - Firefox limits the amount of space that can be used by IndexedDB to a small fraction of the disk space actually - available on your computer. -- Make sure that you have history set to "Remember history" enabled in your privacy settings. When this option is - set to "Never remember history", IndexedDB access is once again disabled for an inexplicable reason. -- As a last resort, try using the [Refresh Firefox](https://support.mozilla.org/en-US/kb/reset-preferences-fix-problems) - feature to reset your user profile. It appears that the Firefox profile system can corrupt itself preventing - IndexedDB from being accessible to Yomitan. - -**Will you add support for online dictionaries?** - -Online dictionaries will not be implemented because it is not possible to support them in a robust way. In order to -perform Japanese deinflection, Yomitan must execute dozens of database queries for every single word. Factoring in -network latency and the fragility of web scraping, it would not be possible to maintain a good and consistent user -experience. - -**Is it possible to use Yomitan with files saved locally on my computer with Chrome?** - -In order to use Yomitan with local files in Chrome, you must first tick the _Allow access to file URLs_ checkbox -for Yomitan on the extensions page. Due to the restrictions placed on browser addons in the WebExtensions model, it -will likely never be possible to use Yomitan with PDF files. - -**Is it possible to delete individual dictionaries without purging the database?** - -Yomitan is able to delete individual dictionaries, but keep in mind that this process can be _very_ slow and can -cause the browser to become unresponsive. The time it takes to delete a single dictionary can sometimes be roughly -the same as the time it originally took to import, which can be significant for certain large dictionaries. - -**Why aren't EPWING dictionaries bundled with Yomitan?** - -The vast majority of EPWING dictionaries are proprietary, so they are unfortunately not able to be included in -this extension due to copyright reasons. - -**When are you going to add support for $MYLANGUAGE?** - -Developing Yomitan requires a decent understanding of Japanese sentence structure and grammar, and other languages -are likely to have their own unique set of rules for syntax, grammar, inflection, and so on. Supporting additional -languages would not only require many additional changes to the codebase, it would also incur significant maintenance -overhead and knowledge demands for the developers. Therefore, suggestions and contributions for supporting -new languages will be declined, allowing Yomitan's focus to remain Japanese-centric. - -## Licenses - -Required licensing notices for this project follow below: - -- **EDRDG License** \ - This package uses the [EDICT](https://www.edrdg.org/jmdict/edict.html) and - [KANJIDIC](https://www.edrdg.org/wiki/index.php/KANJIDIC_Project) dictionary files. These files are the property of - the [Electronic Dictionary Research and Development Group](https://www.edrdg.org/), and are used in conformance with - the Group's [license](https://www.edrdg.org/edrdg/licence.html). - -- **Kanjium License** \ - The pitch accent notation, verb particle data, phonetics, homonyms and other additions or modifications to EDICT, - KANJIDIC or KRADFILE were provided by Uros Ozvatic through his free database. - -## Third-Party Libraries - -Yomitan uses several third-party libraries to function. - -| Name | Installed version | License type | Link | -| :------------------ | :---------------- | :----------- | :------------------------------------------------------- | -| @zip.js/zip.js | 2.7.31 | BSD-3-Clause | git+https://github.com/gildas-lormeau/zip.js.git | -| dexie | 3.2.4 | Apache-2.0 | git+https://github.com/dfahlander/Dexie.js.git | -| dexie-export-import | 4.0.7 | Apache-2.0 | git+https://github.com/dexie/Dexie.js.git | -| handlebars | 4.7.8 | MIT | git+https://github.com/handlebars-lang/handlebars.js.git | -| parse5 | 7.1.2 | MIT | git://github.com/inikulin/parse5.git | -| wanakana | 5.3.1 | MIT | git+ssh://git@github.com/WaniKani/WanaKana.git | +Follow the steps in [yomichan-de](https://github.com/seth-js/yomichan-de). diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 21cc2de2c4..a1a409b703 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2016-2022 Yomichan Authors * @@ -1403,6 +1404,9 @@ export class Backend { * @param {import('settings').OptionsContext} optionsContext * @returns {Promise} */ + + /* https://github.com/seth-js/yomichan-de */ + // Don't use Japanese text segmentation since it breaks things async _textParseScanning(text, scanLength, optionsContext) { // const jp = this._japaneseUtil; // /** @type {import('translator').FindTermsMode} */ From e191c0dbeba123c800c4fcad62db4c3a16f7a79a Mon Sep 17 00:00:00 2001 From: Cashew Date: Sun, 10 Dec 2023 11:56:28 +0900 Subject: [PATCH 03/12] tidy up code --- ext/js/display/display.js | 277 +++++++++++++++++++------------------- 1 file changed, 135 insertions(+), 142 deletions(-) diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 740cff10a4..525f3ab275 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -17,24 +17,60 @@ * along with this program. If not, see . */ -import { Frontend } from '../app/frontend.js'; -import { PopupFactory } from '../app/popup-factory.js'; -import { ThemeController } from '../app/theme-controller.js'; -import { FrameEndpoint } from '../comm/frame-endpoint.js'; -import { DynamicProperty, EventDispatcher, EventListenerCollection, clone, deepEqual, invokeMessageHandler, log, promiseTimeout } from '../core.js'; -import { PopupMenu } from '../dom/popup-menu.js'; -import { ScrollElement } from '../dom/scroll-element.js'; -import { HotkeyHelpController } from '../input/hotkey-help-controller.js'; -import { TextScanner } from '../language/text-scanner.js'; -import { dynamicLoader } from '../script/dynamic-loader.js'; -import { yomitan } from '../yomitan.js'; -import { DisplayContentManager } from './display-content-manager.js'; -import { DisplayGenerator } from './display-generator.js'; -import { DisplayHistory } from './display-history.js'; -import { DisplayNotification } from './display-notification.js'; -import { ElementOverflowController } from './element-overflow-controller.js'; -import { OptionToggleHotkeyHandler } from './option-toggle-hotkey-handler.js'; -import { QueryParser } from './query-parser.js'; +import {Frontend} from '../app/frontend.js'; +import {PopupFactory} from '../app/popup-factory.js'; +import {ThemeController} from '../app/theme-controller.js'; +import {FrameEndpoint} from '../comm/frame-endpoint.js'; +import {DynamicProperty, EventDispatcher, EventListenerCollection, clone, deepEqual, invokeMessageHandler, log, promiseTimeout} from '../core.js'; +import {PopupMenu} from '../dom/popup-menu.js'; +import {ScrollElement} from '../dom/scroll-element.js'; +import {HotkeyHelpController} from '../input/hotkey-help-controller.js'; +import {TextScanner} from '../language/text-scanner.js'; +import {dynamicLoader} from '../script/dynamic-loader.js'; +import {yomitan} from '../yomitan.js'; +import {DisplayContentManager} from './display-content-manager.js'; +import {DisplayGenerator} from './display-generator.js'; +import {DisplayHistory} from './display-history.js'; +import {DisplayNotification} from './display-notification.js'; +import {ElementOverflowController} from './element-overflow-controller.js'; +import {OptionToggleHotkeyHandler} from './option-toggle-hotkey-handler.js'; +import {QueryParser} from './query-parser.js'; + +/** + * + * @param {string} text + * @returns {string} + */ +function firstCharLower(text) { + /** + * @type {string[]} + */ + const chars = []; + + text.split('').forEach((char) => chars.push(char)); + + chars[0] = chars[0].toLowerCase(); + + return chars.join(''); +} + +/** + * + * @param {string} text + * @returns {string} + */ +function firstCharUpper(text) { + /** + * @type {string[]} + */ + const chars = []; + + text.split('').forEach((char) => chars.push(char)); + + chars[0] = chars[0].toUpperCase(); + + return chars.join(''); +} /** * @augments EventDispatcher @@ -69,7 +105,7 @@ export class Display extends EventDispatcher { /** @type {HTMLElement[]} */ this._dictionaryEntryNodes = []; /** @type {import('settings').OptionsContext} */ - this._optionsContext = { depth: 0, url: window.location.href }; + this._optionsContext = {depth: 0, url: window.location.href}; /** @type {?import('settings').ProfileOptions} */ this._options = null; /** @type {number} */ @@ -97,7 +133,7 @@ export class Display extends EventDispatcher { /** @type {import('core').MessageHandlerMap} */ this._windowMessageHandlers = new Map(); /** @type {DisplayHistory} */ - this._history = new DisplayHistory({ clearable: true, useBrowserHistory: false }); + this._history = new DisplayHistory({clearable: true, useBrowserHistory: false}); /** @type {boolean} */ this._historyChangeIgnore = false; /** @type {boolean} */ @@ -209,15 +245,15 @@ export class Display extends EventDispatcher { ['previousEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(-1, true); }] ]); this.registerDirectMessageHandlers([ - ['Display.setOptionsContext', { async: true, handler: this._onMessageSetOptionsContext.bind(this) }], - ['Display.setContent', { async: false, handler: this._onMessageSetContent.bind(this) }], - ['Display.setCustomCss', { async: false, handler: this._onMessageSetCustomCss.bind(this) }], - ['Display.setContentScale', { async: false, handler: this._onMessageSetContentScale.bind(this) }], - ['Display.configure', { async: true, handler: this._onMessageConfigure.bind(this) }], - ['Display.visibilityChanged', { async: false, handler: this._onMessageVisibilityChanged.bind(this) }] + ['Display.setOptionsContext', {async: true, handler: this._onMessageSetOptionsContext.bind(this)}], + ['Display.setContent', {async: false, handler: this._onMessageSetContent.bind(this)}], + ['Display.setCustomCss', {async: false, handler: this._onMessageSetCustomCss.bind(this)}], + ['Display.setContentScale', {async: false, handler: this._onMessageSetContentScale.bind(this)}], + ['Display.configure', {async: true, handler: this._onMessageConfigure.bind(this)}], + ['Display.visibilityChanged', {async: false, handler: this._onMessageVisibilityChanged.bind(this)}] ]); this.registerWindowMessageHandlers([ - ['Display.extensionUnloaded', { async: false, handler: this._onMessageExtensionUnloaded.bind(this) }] + ['Display.extensionUnloaded', {async: false, handler: this._onMessageExtensionUnloaded.bind(this)}] ]); } @@ -308,8 +344,8 @@ export class Display extends EventDispatcher { this._themeController.prepare(); // State setup - const { documentElement } = document; - const { browser } = await yomitan.api.getEnvironmentInfo(); + const {documentElement} = document; + const {browser} = await yomitan.api.getEnvironmentInfo(); this._browser = browser; if (documentElement !== null) { @@ -329,7 +365,7 @@ export class Display extends EventDispatcher { this._progressIndicatorVisible.on('change', this._onProgressIndicatorVisibleChanged.bind(this)); yomitan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this)); yomitan.crossFrame.registerHandlers([ - ['popupMessage', { async: 'dynamic', handler: this._onDirectMessage.bind(this) }] + ['popupMessage', {async: 'dynamic', handler: this._onDirectMessage.bind(this)}] ]); window.addEventListener('message', this._onWindowMessage.bind(this), false); @@ -339,7 +375,7 @@ export class Display extends EventDispatcher { documentElement.addEventListener('auxclick', this._onDocumentElementClick.bind(this), false); } - document.addEventListener('wheel', this._onWheel.bind(this), { passive: false }); + document.addEventListener('wheel', this._onWheel.bind(this), {passive: false}); if (this._closeButton !== null) { this._closeButton.addEventListener('click', this._onCloseButtonClick.bind(this), false); } @@ -372,7 +408,7 @@ export class Display extends EventDispatcher { /** * @param {{clearable?: boolean, useBrowserHistory?: boolean}} details */ - setHistorySettings({ clearable, useBrowserHistory }) { + setHistorySettings({clearable, useBrowserHistory}) { if (typeof clearable !== 'undefined') { this._history.clearable = clearable; } @@ -414,7 +450,7 @@ export class Display extends EventDispatcher { /** */ async updateOptions() { const options = await yomitan.api.optionsGet(this.getOptionsContext()); - const { scanning: scanningOptions, sentenceParsing: sentenceParsingOptions } = options; + const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options; this._options = options; this._updateHotkeys(options); @@ -451,7 +487,7 @@ export class Display extends EventDispatcher { this._updateContentTextScanner(options); /** @type {import('display').OptionsUpdatedEvent} */ - const event = { options }; + const event = {options}; this.trigger('optionsUpdated', event); } @@ -460,7 +496,7 @@ export class Display extends EventDispatcher { * @param {import('display').ContentDetails} details Information about the content to show. */ setContent(details) { - const { focus, params, state, content } = details; + const {focus, params, state, content} = details; const historyMode = this._historyHasChanged ? details.historyMode : 'clear'; if (focus) { @@ -550,7 +586,7 @@ export class Display extends EventDispatcher { const type = this._contentType; if (type === 'clear') { return; } const query = this._query; - const { state } = this._history; + const {state} = this._history; const hasState = typeof state === 'object' && state !== null; /** @type {import('display').HistoryState} */ const newState = ( @@ -560,7 +596,7 @@ export class Display extends EventDispatcher { focusEntry: 0, optionsContext: void 0, url: window.location.href, - sentence: { text: query, offset: 0 }, + sentence: {text: query, offset: 0}, documentTitle: document.title } ); @@ -616,7 +652,7 @@ export class Display extends EventDispatcher { getElementDictionaryEntryIndex(element) { const node = /** @type {?HTMLElement} */ (element.closest('.entry')); if (node === null) { return -1; } - const { index } = node.dataset; + const {index} = node.dataset; if (typeof index !== 'string') { return -1; } const indexNumber = parseInt(index, 10); return Number.isFinite(indexNumber) ? indexNumber : -1; @@ -643,13 +679,13 @@ export class Display extends EventDispatcher { * @throws {Error} */ _onDirectMessage(data) { - const { action, params } = this._authenticateMessageData(data); + const {action, params} = this._authenticateMessageData(data); const handlerInfo = this._directMessageHandlers.get(action); if (typeof handlerInfo === 'undefined') { throw new Error(`Invalid action: ${action}`); } - const { async, handler } = handlerInfo; + const {async, handler} = handlerInfo; const result = handler(params); return { async: typeof async === 'boolean' && async, @@ -660,7 +696,7 @@ export class Display extends EventDispatcher { /** * @param {MessageEvent>} details */ - _onWindowMessage({ data }) { + _onWindowMessage({data}) { let data2; try { data2 = this._authenticateMessageData(data); @@ -668,7 +704,7 @@ export class Display extends EventDispatcher { return; } - const { action, params } = data2; + const {action, params} = data2; const messageHandler = this._windowMessageHandlers.get(action); if (typeof messageHandler === 'undefined') { return; } @@ -679,7 +715,7 @@ export class Display extends EventDispatcher { /** * @param {{optionsContext: import('settings').OptionsContext}} details */ - async _onMessageSetOptionsContext({ optionsContext }) { + async _onMessageSetOptionsContext({optionsContext}) { await this.setOptionsContext(optionsContext); this.searchLast(true); } @@ -687,28 +723,28 @@ export class Display extends EventDispatcher { /** * @param {{details: import('display').ContentDetails}} details */ - _onMessageSetContent({ details }) { + _onMessageSetContent({details}) { this.setContent(details); } /** * @param {{css: string}} details */ - _onMessageSetCustomCss({ css }) { + _onMessageSetCustomCss({css}) { this.setCustomCss(css); } /** * @param {{scale: number}} details */ - _onMessageSetContentScale({ scale }) { + _onMessageSetContentScale({scale}) { this._setContentScale(scale); } /** * @param {import('display').ConfigureMessageDetails} details */ - async _onMessageConfigure({ depth, parentPopupId, parentFrameId, childrenSupported, scale, optionsContext }) { + async _onMessageConfigure({depth, parentPopupId, parentFrameId, childrenSupported, scale, optionsContext}) { this._depth = depth; this._parentPopupId = parentPopupId; this._parentFrameId = parentFrameId; @@ -720,10 +756,10 @@ export class Display extends EventDispatcher { /** * @param {{value: boolean}} details */ - _onMessageVisibilityChanged({ value }) { + _onMessageVisibilityChanged({value}) { this._frameVisible = value; /** @type {import('display').FrameVisibilityChangeEvent} */ - const event = { value }; + const event = {value}; this.trigger('frameVisibilityChange', event); } @@ -804,7 +840,7 @@ export class Display extends EventDispatcher { /** * @param {import('display').QueryParserSearchedEvent} details */ - _onQueryParserSearch({ type, dictionaryEntries, sentence, inputInfo: { eventType }, textSource, optionsContext, sentenceOffset }) { + _onQueryParserSearch({type, dictionaryEntries, sentence, inputInfo: {eventType}, textSource, optionsContext, sentenceOffset}) { const query = textSource.text(); const historyState = this._history.state; const historyMode = ( @@ -838,7 +874,7 @@ export class Display extends EventDispatcher { const details = { focus: false, historyMode: 'clear', - params: { type }, + params: {type}, state: {}, content: { contentOrigin: { @@ -877,7 +913,7 @@ export class Display extends EventDispatcher { /** * @param {import('dynamic-property').ChangeEventDetails} details */ - _onProgressIndicatorVisibleChanged({ value }) { + _onProgressIndicatorVisibleChanged({value}) { if (this._progressIndicatorTimer !== null) { clearTimeout(this._progressIndicatorTimer); this._progressIndicatorTimer = null; @@ -902,10 +938,10 @@ export class Display extends EventDispatcher { async _onKanjiLookup(e) { try { e.preventDefault(); - const { state } = this._history; + const {state} = this._history; if (!(typeof state === 'object' && state !== null)) { return; } - let { sentence, url, documentTitle } = state; + let {sentence, url, documentTitle} = state; if (typeof url !== 'string') { url = window.location.href; } if (typeof documentTitle !== 'string') { documentTitle = document.title; } const optionsContext = this.getOptionsContext(); @@ -1020,7 +1056,7 @@ export class Display extends EventDispatcher { _onEntryClick(e) { if (e.button !== 0) { return; } const node = /** @type {HTMLElement} */ (e.currentTarget); - const { index } = node.dataset; + const {index} = node.dataset; if (typeof index !== 'string') { return; } const indexNumber = parseInt(index, 10); if (!Number.isFinite(indexNumber)) { return; } @@ -1067,7 +1103,7 @@ export class Display extends EventDispatcher { */ _onMenuButtonMenuClose(e) { const node = /** @type {HTMLElement} */ (e.currentTarget); - const { action } = e.detail; + const {action} = e.detail; switch (action) { case 'log-debug-info': this._logDictionaryEntryData(this.getElementDictionaryEntryIndex(node)); @@ -1128,8 +1164,8 @@ export class Display extends EventDispatcher { * @param {import('settings').ProfileOptions} options */ _setTheme(options) { - const { general } = options; - const { popupTheme } = general; + const {general} = options; + const {popupTheme} = general; this._themeController.theme = popupTheme; this._themeController.outerTheme = general.popupOuterTheme; this._themeController.updateTheme(); @@ -1174,47 +1210,11 @@ export class Display extends EventDispatcher { */ const dictionaryEntries = []; - /** - * - * @param {string} text - * @returns {string} - */ - function firstCharLower(text) { - /** - * @type {string[]} - */ - let chars = []; - - text.split('').forEach((char) => chars.push(char)); - - chars[0] = chars[0].toLowerCase(); - - return chars.join(''); - } - - /** - * - * @param {string} text - * @returns {string} - */ - function firstCharUpper(text) { - /** - * @type {string[]} - */ - let chars = []; - - text.split('').forEach((char) => chars.push(char)); - - chars[0] = chars[0].toUpperCase(); - - return chars.join(''); - } - const searches = [ firstCharLower(source), firstCharUpper(source), source.toLowerCase(), - source, + source ]; // handle english apostrophe @@ -1227,12 +1227,12 @@ export class Display extends EventDispatcher { const result = await yomitan.api.termsFind( search, findDetails, - optionsContext, + optionsContext ); if (result.dictionaryEntries.length > 0) { result.dictionaryEntries.forEach((entry) => { - const { definitions } = entry; + const {definitions} = entry; // avoid duplicate results if (!matchedDefs.includes(JSON.stringify(definitions))) { @@ -1244,7 +1244,6 @@ export class Display extends EventDispatcher { } return dictionaryEntries; - } } @@ -1270,7 +1269,7 @@ export class Display extends EventDispatcher { } this._setQuery(query, queryFull, queryOffset); - let { state, content } = this._history; + let {state, content} = this._history; let changeHistory = false; if (!(typeof content === 'object' && content !== null)) { content = {}; @@ -1281,7 +1280,7 @@ export class Display extends EventDispatcher { changeHistory = true; } - let { focusEntry, scrollX, scrollY, optionsContext } = state; + let {focusEntry, scrollX, scrollY, optionsContext} = state; if (typeof focusEntry !== 'number') { focusEntry = 0; } if (!(typeof optionsContext === 'object' && optionsContext !== null)) { optionsContext = this.getOptionsContext(); @@ -1289,7 +1288,7 @@ export class Display extends EventDispatcher { changeHistory = true; } - let { dictionaryEntries } = content; + let {dictionaryEntries} = content; if (!Array.isArray(dictionaryEntries)) { dictionaryEntries = lookup && query.length > 0 ? await this._findDictionaryEntries(type === 'kanji', query, wildcardsEnabled, optionsContext) : []; if (this._setContentToken !== token) { return; } @@ -1298,9 +1297,9 @@ export class Display extends EventDispatcher { } let contentOriginValid = false; - const { contentOrigin } = content; + const {contentOrigin} = content; if (typeof contentOrigin === 'object' && contentOrigin !== null) { - const { tabId, frameId } = contentOrigin; + const {tabId, frameId} = contentOrigin; if (typeof tabId === 'number' && typeof frameId === 'number') { this._contentOriginTabId = tabId; this._contentOriginFrameId = frameId; @@ -1359,7 +1358,7 @@ export class Display extends EventDispatcher { } if (typeof scrollX === 'number' || typeof scrollY === 'number') { - let { x, y } = this._windowScroll; + let {x, y} = this._windowScroll; if (typeof scrollX === 'number') { x = scrollX; } if (typeof scrollY === 'number') { y = scrollY; } this._windowScroll.stop(); @@ -1371,37 +1370,35 @@ export class Display extends EventDispatcher { const formBoxes = {}; for (const inflectElem of entryElem.querySelectorAll('.inflection')) { - if (inflectElem == undefined) return; - if (inflectElem.textContent == undefined) return; + if (inflectElem.textContent === null) { return; } const [targetPOS] = inflectElem.textContent.split(' '); - let _inflectText = inflectElem.textContent.split(' '); - _inflectText.shift(); - let inflectText = _inflectText.join(' '); + const inflectTextArr = inflectElem.textContent.split(' '); + inflectTextArr.shift(); + let inflectText = inflectTextArr.join(' '); - if (!formBoxes[targetPOS]) formBoxes[targetPOS] = {}; - if (!formBoxes[targetPOS]['inflections']) - formBoxes[targetPOS]['inflections'] = []; - formBoxes[targetPOS]['isAutomated'] = false; + if (!formBoxes[targetPOS]) { formBoxes[targetPOS] = {}; } + if (!formBoxes[targetPOS].inflections) { formBoxes[targetPOS].inflections = []; } + formBoxes[targetPOS].isAutomated = false; if (/-automated-/.test(inflectText)) { inflectText = inflectText.replace(/^-.+?- /, ''); - formBoxes[targetPOS]['isAutomated'] = true; + formBoxes[targetPOS].isAutomated = true; } const pointerText = inflectText.replace(/\}.+/, '').replace(/\{/, ''); inflectText = inflectText.replace(/\{.+?\} /, ''); - formBoxes[targetPOS]['pointerText'] = pointerText; + formBoxes[targetPOS].pointerText = pointerText; - formBoxes[targetPOS]['inflections'].push(inflectText); + formBoxes[targetPOS].inflections.push(inflectText); } for (const defElem of Array.from(entryElem.querySelectorAll('.definition-item'))) { for (const tagElem of Array.from(defElem.querySelectorAll('.tag[data-category="partOfSpeech"]'))) { const pos = tagElem.textContent; - if (pos == null) return; + if (pos === null) { return; } if (formBoxes[pos]) { const formInfoBox = document.createElement('div'); @@ -1430,16 +1427,14 @@ export class Display extends EventDispatcher { formInfoBox.appendChild(reasonList); const definitionTagList = defElem.querySelector('.definition-tag-list'); - if (definitionTagList == undefined) return; + if (definitionTagList === null) { return; } definitionTagList.append(formInfoBox); } } if (defElem.querySelector('.form-info-box')) { defElem.addEventListener('mouseleave', (e) => { - for (const boxElem of Array.from(defElem.querySelectorAll('.form-info-box'))) { - // Element does not have a 'style' property, but HTMLElement does implement it - // @ts-ignore + for (const boxElem of /** @type {NodeListOf} */ (defElem.querySelectorAll('.form-info-box'))) { boxElem.style.display = 'none'; } }); @@ -1449,15 +1444,13 @@ export class Display extends EventDispatcher { showBoxIcon.classList.add('show-info-btn'); showBoxIcon.addEventListener('mouseenter', (e) => { - for (const boxElem of Array.from(defElem.querySelectorAll('.form-info-box'))) { - // Element does not have a 'style' property, but HTMLElement does implement it - // @ts-ignore + for (const boxElem of /** @type {NodeListOf} */ (defElem.querySelectorAll('.form-info-box'))) { boxElem.style.display = 'block'; } }); - let formIntoBox = defElem.querySelector('.form-info-box'); - if (formIntoBox == undefined) return; + const formIntoBox = defElem.querySelector('.form-info-box'); + if (formIntoBox === null) { return; } formIntoBox.before(showBoxIcon); } } @@ -1574,7 +1567,7 @@ export class Display extends EventDispatcher { * @param {boolean} next */ _updateNavigation(previous, next) { - const { documentElement } = document; + const {documentElement} = document; if (documentElement !== null) { documentElement.dataset.hasNavigationPrevious = `${previous}`; documentElement.dataset.hasNavigationNext = `${next}`; @@ -1654,11 +1647,11 @@ export class Display extends EventDispatcher { let focusDefinitionIndex = null; if (dictionaryEntry.type === 'term') { - const { dictionary } = dictionaryEntry.definitions[visibleDefinitionIndex]; + const {dictionary} = dictionaryEntry.definitions[visibleDefinitionIndex]; for (let i = index; i >= 0 && i < count; i += sign) { const otherDictionaryEntry = this._dictionaryEntries[i]; if (otherDictionaryEntry.type !== 'term') { continue; } - const { definitions } = otherDictionaryEntry; + const {definitions} = otherDictionaryEntry; const jj = definitions.length; let j = (i === index ? visibleDefinitionIndex + sign : (sign > 0 ? 0 : jj - 1)); for (; j >= 0 && j < jj; j += sign) { @@ -1684,9 +1677,9 @@ export class Display extends EventDispatcher { * @returns {?number} */ _getDictionaryEntryVisibleDefinitionIndex(index, sign) { - const { top: scrollTop, bottom: scrollBottom } = this._windowScroll.getRect(); + const {top: scrollTop, bottom: scrollBottom} = this._windowScroll.getRect(); - const { definitions } = this._dictionaryEntries[index]; + const {definitions} = this._dictionaryEntries[index]; const nodes = this._getDictionaryEntryDefinitionNodes(index); const definitionCount = Math.min(definitions.length, nodes.length); if (definitionCount <= 0) { return null; } @@ -1694,7 +1687,7 @@ export class Display extends EventDispatcher { let visibleIndex = null; let visibleCoverage = 0; for (let i = (sign > 0 ? 0 : definitionCount - 1); i >= 0 && i < definitionCount; i += sign) { - const { top, bottom } = nodes[i].getBoundingClientRect(); + const {top, bottom} = nodes[i].getBoundingClientRect(); if (bottom <= scrollTop || top >= scrollBottom) { continue; } const top2 = Math.max(scrollTop, Math.min(scrollBottom, top)); const bottom2 = Math.max(scrollTop, Math.min(scrollBottom, bottom)); @@ -1759,7 +1752,7 @@ export class Display extends EventDispatcher { /** */ _updateHistoryState() { - const { state, content } = this._history; + const {state, content} = this._history; if (!(typeof state === 'object' && state !== null)) { return; } state.focusEntry = this._index; @@ -2035,12 +2028,12 @@ export class Display extends EventDispatcher { this._contentTextScanner.on('searched', this._onContentTextScannerSearched.bind(this)); } - const { scanning: scanningOptions, sentenceParsing: sentenceParsingOptions } = options; + const {scanning: scanningOptions, sentenceParsing: sentenceParsingOptions} = options; this._contentTextScanner.setOptions({ inputs: [{ include: 'mouse0', exclude: '', - types: { mouse: true, pen: false, touch: false }, + types: {mouse: true, pen: false, touch: false}, options: { searchTerms: true, searchKanji: true, @@ -2079,7 +2072,7 @@ export class Display extends EventDispatcher { /** * @param {import('text-scanner').SearchedEventDetails} details */ - _onContentTextScannerSearched({ type, dictionaryEntries, sentence, textSource, optionsContext, error }) { + _onContentTextScannerSearched({type, dictionaryEntries, sentence, textSource, optionsContext, error}) { if (error !== null && !yomitan.isExtensionUnloaded) { log.error(error); } @@ -2118,7 +2111,7 @@ export class Display extends EventDispatcher { * @type {import('display').GetSearchContextCallback} */ _getSearchContext() { - return { optionsContext: this.getOptionsContext() }; + return {optionsContext: this.getOptionsContext()}; } /** @@ -2194,12 +2187,12 @@ export class Display extends EventDispatcher { async _logDictionaryEntryData(index) { if (index < 0 || index >= this._dictionaryEntries.length) { return; } const dictionaryEntry = this._dictionaryEntries[index]; - const result = { dictionaryEntry }; + const result = {dictionaryEntry}; /** @type {Promise[]} */ const promises = []; /** @type {import('display').LogDictionaryEntryDataEvent} */ - const event = { dictionaryEntry, promises }; + const event = {dictionaryEntry, promises}; this.trigger('logDictionaryEntryData', event); if (promises.length > 0) { for (const result2 of await Promise.all(promises)) { @@ -2218,7 +2211,7 @@ export class Display extends EventDispatcher { /** */ _triggerContentUpdateStart() { /** @type {import('display').ContentUpdateStartEvent} */ - const event = { type: this._contentType, query: this._query }; + const event = {type: this._contentType, query: this._query}; this.trigger('contentUpdateStart', event); } @@ -2229,14 +2222,14 @@ export class Display extends EventDispatcher { */ _triggerContentUpdateEntry(dictionaryEntry, element, index) { /** @type {import('display').ContentUpdateEntryEvent} */ - const event = { dictionaryEntry, element, index }; + const event = {dictionaryEntry, element, index}; this.trigger('contentUpdateEntry', event); } /** */ _triggerContentUpdateComplete() { /** @type {import('display').ContentUpdateCompleteEvent} */ - const event = { type: this._contentType }; + const event = {type: this._contentType}; this.trigger('contentUpdateComplete', event); } } From acad594c8a2a08d0a66ef44a51a279b6c1e870b8 Mon Sep 17 00:00:00 2001 From: Cashew Date: Sun, 10 Dec 2023 12:42:51 +0900 Subject: [PATCH 04/12] opt for Map instead of Object --- ext/js/display/display.js | 24 ++-- ext/js/language/translator.js | 262 +++++++++++++++++----------------- 2 files changed, 141 insertions(+), 145 deletions(-) diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 525f3ab275..b6cdcf9228 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -1367,7 +1367,7 @@ export class Display extends EventDispatcher { /* https://github.com/seth-js/yomichan-de */ for (const entryElem of Array.from(document.querySelectorAll('#dictionary-entries .entry'))) { - const formBoxes = {}; + const formBoxes = new Map(); for (const inflectElem of entryElem.querySelectorAll('.inflection')) { if (inflectElem.textContent === null) { return; } @@ -1378,33 +1378,35 @@ export class Display extends EventDispatcher { inflectTextArr.shift(); let inflectText = inflectTextArr.join(' '); - if (!formBoxes[targetPOS]) { formBoxes[targetPOS] = {}; } - if (!formBoxes[targetPOS].inflections) { formBoxes[targetPOS].inflections = []; } - formBoxes[targetPOS].isAutomated = false; + if (typeof formBoxes.get(targetPOS) === 'undefined') { formBoxes.set(targetPOS, {}); } + const targetPOSBox = formBoxes.get(targetPOS); + + if (!targetPOSBox.inflections) { targetPOSBox.inflections = []; } + targetPOSBox.isAutomated = false; if (/-automated-/.test(inflectText)) { inflectText = inflectText.replace(/^-.+?- /, ''); - formBoxes[targetPOS].isAutomated = true; + targetPOSBox.isAutomated = true; } const pointerText = inflectText.replace(/\}.+/, '').replace(/\{/, ''); inflectText = inflectText.replace(/\{.+?\} /, ''); - formBoxes[targetPOS].pointerText = pointerText; + targetPOSBox.pointerText = pointerText; - formBoxes[targetPOS].inflections.push(inflectText); + targetPOSBox.inflections.push(inflectText); } for (const defElem of Array.from(entryElem.querySelectorAll('.definition-item'))) { for (const tagElem of Array.from(defElem.querySelectorAll('.tag[data-category="partOfSpeech"]'))) { const pos = tagElem.textContent; if (pos === null) { return; } - if (formBoxes[pos]) { + if (formBoxes.get(pos)) { const formInfoBox = document.createElement('div'); formInfoBox.classList.add('form-info-box'); - if (formBoxes[pos].isAutomated) { + if (formBoxes.get(pos).isAutomated) { const automatedNotice = document.createElement('div'); automatedNotice.classList.add('automated-result-text'); automatedNotice.textContent = '(automated results)'; @@ -1413,12 +1415,12 @@ export class Display extends EventDispatcher { const pointerElem = document.createElement('div'); pointerElem.classList.add('pointer-text'); - pointerElem.textContent = formBoxes[pos].pointerText; + pointerElem.textContent = formBoxes.get(pos).pointerText; formInfoBox.appendChild(pointerElem); const reasonList = document.createElement('ol'); - for (const reason of formBoxes[pos].inflections) { + for (const reason of formBoxes.get(pos).inflections) { const item = document.createElement('li'); item.textContent = reason; reasonList.appendChild(item); diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 618f867526..3647e2cb62 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -17,9 +17,9 @@ * along with this program. If not, see . */ -import { RegexUtil } from '../general/regex-util.js'; -import { TextSourceMap } from '../general/text-source-map.js'; -import { Deinflector } from './deinflector.js'; +import {RegexUtil} from '../general/regex-util.js'; +import {TextSourceMap} from '../general/text-source-map.js'; +import {Deinflector} from './deinflector.js'; /** * Class which finds term and kanji dictionary entries for text. @@ -29,7 +29,7 @@ export class Translator { * Creates a new Translator instance. * @param {import('translator').ConstructorDetails} details The details for the class. */ - constructor({ japaneseUtil, database }) { + constructor({japaneseUtil, database}) { /** @type {import('./sandbox/japanese-util.js').JapaneseUtil} */ this._japaneseUtil = japaneseUtil; /** @type {import('./dictionary-database.js').DictionaryDatabase} */ @@ -69,9 +69,9 @@ export class Translator { * @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; + const {enabledDictionaryMap, excludeDictionaryDefinitions, sortFrequencyDictionary, sortFrequencyDictionaryOrder} = options; const tagAggregator = new TranslatorTagAggregator(); - let { dictionaryEntries, originalTextLength } = await this._findTermsInternal(text, enabledDictionaryMap, options, tagAggregator); + let {dictionaryEntries, originalTextLength} = await this._findTermsInternal(text, enabledDictionaryMap, options, tagAggregator); switch (mode) { case 'group': @@ -107,14 +107,14 @@ export class Translator { if (dictionaryEntries.length > 1) { this._sortTermDictionaryEntries(dictionaryEntries); } - for (const { definitions, frequencies, pronunciations } of dictionaryEntries) { + for (const {definitions, frequencies, pronunciations} of dictionaryEntries) { this._flagRedundantDefinitionTags(definitions); if (definitions.length > 1) { this._sortTermDictionaryEntryDefinitions(definitions); } if (frequencies.length > 1) { this._sortTermDictionaryEntrySimpleData(frequencies); } if (pronunciations.length > 1) { this._sortTermDictionaryEntrySimpleData(pronunciations); } } - return { dictionaryEntries, originalTextLength }; + return {dictionaryEntries, originalTextLength}; } /** @@ -126,7 +126,7 @@ export class Translator { * @returns {Promise} An array of definitions. See the _createKanjiDefinition() function for structure details. */ async findKanji(text, options) { - const { enabledDictionaryMap } = options; + const {enabledDictionaryMap} = options; const kanjiUnique = new Set(); for (const c of text) { kanjiUnique.add(c); @@ -140,7 +140,7 @@ export class Translator { /** @type {import('dictionary').KanjiDictionaryEntry[]} */ const dictionaryEntries = []; const tagAggregator = new TranslatorTagAggregator(); - for (const { character, onyomi, kunyomi, tags, definitions, stats, dictionary } of databaseEntries) { + for (const {character, onyomi, kunyomi, tags, definitions, stats, dictionary} of databaseEntries) { const expandedStats = await this._expandKanjiStats(stats, dictionary); const dictionaryEntry = this._createKanjiDictionaryEntry(character, dictionary, onyomi, kunyomi, expandedStats, definitions); dictionaryEntries.push(dictionaryEntry); @@ -169,21 +169,21 @@ export class Translator { dictionarySet.add(dictionary); } - const termList = termReadingList.map(({ term }) => term); + 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) { + for (const {mode, data, dictionary, index} of metas) { if (mode !== 'freq') { continue; } - let { term, reading } = termReadingList[index]; + let {term, reading} = termReadingList[index]; 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); + const {frequency: frequencyValue, displayValue, displayValueParsed} = this._getFrequencyInfo(frequency); results.push({ term, reading, @@ -211,7 +211,7 @@ export class Translator { text = this._getJapaneseOnlyText(text); } if (text.length === 0) { - return { dictionaryEntries: [], originalTextLength: 0 }; + return {dictionaryEntries: [], originalTextLength: 0}; } // Makes it so that Yomichan doesn't look up parts of a word, only the full word @@ -231,17 +231,16 @@ export class Translator { let smallestMatch = ''; - deinflections.forEach(flect => { - const { originalText } = flect; + deinflections.forEach((flect) => { + const {originalText} = flect; if (!/\s/.test(originalText) && /\p{L}$/u.test(originalText) - && !smallestMatch) - smallestMatch = originalText; + && !smallestMatch) { smallestMatch = originalText; } }); - deinflections.forEach(flect => { - const { originalText, databaseEntries } = flect; + deinflections.forEach((flect) => { + const {originalText, databaseEntries} = flect; if (databaseEntries && databaseEntries.length > 0) { if (!smallestMatch.includes(originalText) || smallestMatch === originalText) { @@ -259,25 +258,23 @@ export class Translator { do { searching = false; - for (const { databaseEntries } of deinflections) { + for (const {databaseEntries} of deinflections) { for (const ent of databaseEntries) { - - const { definitionTags, definitions, term } = ent; + const {definitionTags, definitions, term} = ent; if (definitionTags.includes('non-lemma')) { ent.skip = true; for (const definition of definitions) { - const lemma = (typeof definition == 'string') ? definition.replace(/.+?\(->(?=.+?\)$)/, '').replace(/\)$/, '') : ''; - const reason = (typeof definition == 'string') ? definition.replace(/\s\(->.+/, '') : ''; + const lemma = (typeof definition === 'string') ? definition.replace(/.+?\(->(?=.+?\)$)/, '').replace(/\)$/, '') : ''; + const reason = (typeof definition === 'string') ? definition.replace(/\s\(->.+/, '') : ''; if (!requiredSearches[lemma]) { searching = true; - requiredSearches[lemma] = { form: term, reasons: [reason] }; - } - else if (!requiredSearches[lemma]["reasons"].includes(reason)) { + requiredSearches[lemma] = {form: term, reasons: [reason]}; + } else if (!requiredSearches[lemma].reasons.includes(reason)) { searching = true; - requiredSearches[lemma]["reasons"].push(reason); + requiredSearches[lemma].reasons.push(reason); } } } @@ -286,7 +283,7 @@ export class Translator { const extraDeinflections = []; - for (const [lemma, { form, reasons }] of Object.entries(requiredSearches)) { + for (const [lemma, {form, reasons}] of Object.entries(requiredSearches)) { const flections = await this._findTermsInternal2(lemma, enabledDictionaryMap, options); /** @@ -294,48 +291,47 @@ export class Translator { */ const filteredFlections = []; - let smallestMatch = ''; + let innerSmallestMatch = ''; - flections.forEach(flect => { - const { originalText } = flect; + flections.forEach((flect) => { + const {originalText} = flect; if (!/\s/.test(originalText) && /\p{L}$/u.test(originalText) - && !smallestMatch) - smallestMatch = originalText; + && !innerSmallestMatch) { innerSmallestMatch = originalText; } }); - flections.forEach(flect => { - const { originalText, deinflectedText, databaseEntries } = flect; + flections.forEach((flect) => { + const {originalText, deinflectedText, databaseEntries} = flect; if (databaseEntries && databaseEntries.length > 0 && lemma === deinflectedText) { - if (!smallestMatch.includes(originalText) || smallestMatch === originalText) { + if (!innerSmallestMatch.includes(originalText) || innerSmallestMatch === originalText) { filteredFlections.push(flect); } } }); for (const flect of filteredFlections) { - const { databaseEntries } = flect; + const {databaseEntries} = flect; - flect['originalText'] = form; + flect.originalText = form; - databaseEntries.forEach(ent => { - const { definitionTags } = ent; + databaseEntries.forEach((ent) => { + const {definitionTags} = ent; - if (definitionTags.includes('non-lemma')) ent.skip = true; + if (definitionTags.includes('non-lemma')) { ent.skip = true; } }); reasons.forEach((/** @type {string} */ reason) => { flect.reasons.push(reason); }); - flect['isExtra'] = true; + flect.isExtra = true; extraDeinflections.push(flect); } } - if (extraDeinflections.length > 0) deinflections.push(...extraDeinflections); + if (extraDeinflections.length > 0) { deinflections.push(...extraDeinflections); } } while (searching); let originalTextLength = 0; @@ -349,9 +345,9 @@ export class Translator { const uniqueResultsObj = {}; const uniqueResults = []; - for (const { databaseEntries, originalText, transformedText, deinflectedText, reasons, isExtra } of deinflections) { - if (!uniqueResultsObj[deinflectedText]) uniqueResultsObj[deinflectedText] = {}; - if (!uniqueResultsObj[deinflectedText][originalText]) uniqueResultsObj[deinflectedText][originalText] = { databaseEntries, originalText, transformedText, deinflectedText, isExtra }; + for (const {databaseEntries, originalText, transformedText, deinflectedText, reasons, isExtra} of deinflections) { + if (!uniqueResultsObj[deinflectedText]) { uniqueResultsObj[deinflectedText] = {}; } + if (!uniqueResultsObj[deinflectedText][originalText]) { uniqueResultsObj[deinflectedText][originalText] = {databaseEntries, originalText, transformedText, deinflectedText, isExtra}; } if (reasons.length > 0) { uniqueResultsObj[deinflectedText][originalText].reasons = [...reasons]; @@ -360,21 +356,20 @@ export class Translator { } } - for (const { originalText, deinflectedText } of deinflections) { + for (const {originalText, deinflectedText} of deinflections) { if (originalText === deinflectedText && Object.entries(uniqueResultsObj[deinflectedText]).length > 1) { delete uniqueResultsObj[deinflectedText][originalText]; } } for (const [lemma, info] of Object.entries(uniqueResultsObj)) { - const [[surface, { databaseEntries, transformedText, reasons, isExtra }]] = Object.entries(info) - uniqueResults.push({ databaseEntries, originalText: surface, transformedText, deinflectedText: lemma, reasons, isExtra }); + const [[surface, {databaseEntries, transformedText, reasons, isExtra}]] = Object.entries(info); + uniqueResults.push({databaseEntries, originalText: surface, transformedText, deinflectedText: lemma, reasons, isExtra}); } // console.log(uniqueResults); - for (const { databaseEntries, originalText, transformedText, deinflectedText, reasons, isExtra } of uniqueResults) { - + for (const {databaseEntries, originalText, transformedText, deinflectedText, reasons, isExtra} of uniqueResults) { if (databaseEntries.length === 0) { continue; } // Makes it so that the character length of lemmas don't affect the non-lemma match @@ -385,7 +380,7 @@ export class Translator { } for (const databaseEntry of databaseEntries) { - const { id } = databaseEntry; + const {id} = databaseEntry; if (ids.has(id)) { continue; } // Makes it so that non-lemma entries aren't added to the dictionary entries @@ -400,11 +395,10 @@ export class Translator { dictionaryEntries.push(dictionaryEntry); ids.add(id); } - } } - return { dictionaryEntries, originalTextLength }; + return {dictionaryEntries, originalTextLength}; } /** @@ -436,7 +430,7 @@ export class Translator { deinflectionArray.push(deinflection); } - const { matchType } = options; + const {matchType} = options; const databaseEntries = await this._database.findTermsBulk(uniqueDeinflectionTerms, enabledDictionaryMap, matchType); for (const databaseEntry of databaseEntries) { @@ -505,7 +499,7 @@ 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 /** @type {Deinflector} */ (this._deinflector).deinflect(source)) { + for (const {term, rules, reasons} of /** @type {Deinflector} */ (this._deinflector).deinflect(source)) { deinflections.push(this._createDeinflection(rawSource, source, term, rules, reasons)); } } @@ -520,7 +514,7 @@ export class Translator { * @returns {string} */ _applyTextReplacements(text, sourceMap, replacements) { - for (const { pattern, replacement } of replacements) { + for (const {pattern, replacement} of replacements) { text = RegexUtil.applyTextReplacement(text, sourceMap, pattern, replacement); } return text; @@ -589,7 +583,7 @@ export class Translator { * @returns {import('translation-internal').DatabaseDeinflection} */ _createDeinflection(originalText, transformedText, deinflectedText, rules, reasons) { - return { originalText, transformedText, deinflectedText, rules, reasons, databaseEntries: [], isExtra: false }; + return {originalText, transformedText, deinflectedText, rules, reasons, databaseEntries: [], isExtra: false}; } // Term dictionary entry grouping @@ -611,7 +605,7 @@ export class Translator { /** @type {Map} */ const ungroupedDictionaryEntriesMap = new Map(); for (const dictionaryEntry of dictionaryEntries) { - const { definitions: [{ id, dictionary, sequences: [sequence] }] } = dictionaryEntry; + const {definitions: [{id, dictionary, sequences: [sequence]}]} = dictionaryEntry; if (mainDictionary === dictionary && sequence >= 0) { let group = groupedDictionaryEntriesMap.get(sequence); if (typeof group === 'undefined') { @@ -619,7 +613,7 @@ export class Translator { ids: new Set(), dictionaryEntries: [] }; - sequenceList.push({ query: sequence, dictionary }); + sequenceList.push({query: sequence, dictionary}); groupedDictionaryEntries.push(group); groupedDictionaryEntriesMap.set(sequence, group); } @@ -659,11 +653,11 @@ export class Translator { 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]; - const { id } = databaseEntry; + const {dictionaryEntries, ids} = groupedDictionaryEntries[databaseEntry.index]; + const {id} = databaseEntry; if (ids.has(id)) { continue; } - const { term } = databaseEntry; + const {term} = databaseEntry; const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, term, term, term, [], false, enabledDictionaryMap, tagAggregator); dictionaryEntries.push(dictionaryEntry); ids.add(id); @@ -686,9 +680,9 @@ export class Translator { const targetMap = new Map(); for (const group of groupedDictionaryEntries) { - const { dictionaryEntries } = group; + const {dictionaryEntries} = group; for (const dictionaryEntry of dictionaryEntries) { - const { term, reading } = dictionaryEntry.headwords[0]; + const {term, reading} = dictionaryEntry.headwords[0]; const key = this._createMapKey([term, reading]); let target = targetMap.get(key); if (typeof target === 'undefined') { @@ -696,7 +690,7 @@ export class Translator { groups: [] }; targetMap.set(key, target); - termList.push({ term, reading }); + termList.push({term, reading}); targetList.push(target); } target.groups.push(group); @@ -705,12 +699,12 @@ export class Translator { // Group unsequenced dictionary entries with sequenced entries that have a matching [term, reading]. for (const [id, dictionaryEntry] of ungroupedDictionaryEntriesMap.entries()) { - const { term, reading } = dictionaryEntry.headwords[0]; + const {term, reading} = dictionaryEntry.headwords[0]; const key = this._createMapKey([term, reading]); const target = targetMap.get(key); if (typeof target === 'undefined') { continue; } - for (const { ids, dictionaryEntries } of target.groups) { + for (const {ids, dictionaryEntries} of target.groups) { if (ids.has(id)) { continue; } dictionaryEntries.push(dictionaryEntry); ids.add(id); @@ -725,10 +719,10 @@ export class Translator { this._sortDatabaseEntriesByIndex(databaseEntries); for (const databaseEntry of databaseEntries) { - const { index, id } = databaseEntry; + const {index, id} = databaseEntry; const sourceText = termList[index].term; const target = targetList[index]; - for (const { ids, dictionaryEntries } of target.groups) { + for (const {ids, dictionaryEntries} of target.groups) { if (ids.has(id)) { continue; } const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, sourceText, sourceText, sourceText, [], false, enabledDictionaryMap, tagAggregator); @@ -747,7 +741,7 @@ export class Translator { _groupDictionaryEntriesByHeadword(dictionaryEntries, tagAggregator) { const groups = new Map(); for (const dictionaryEntry of dictionaryEntries) { - const { inflections, headwords: [{ term, reading }] } = dictionaryEntry; + const {inflections, headwords: [{term, reading}]} = dictionaryEntry; const key = this._createMapKey([term, reading, ...inflections]); let groupDictionaryEntries = groups.get(key); if (typeof groupDictionaryEntries === 'undefined') { @@ -773,7 +767,7 @@ export class Translator { _removeExcludedDefinitions(dictionaryEntries, excludeDictionaryDefinitions) { for (let i = dictionaryEntries.length - 1; i >= 0; --i) { const dictionaryEntry = dictionaryEntries[i]; - const { definitions, pronunciations, frequencies, headwords } = dictionaryEntry; + const {definitions, pronunciations, frequencies, headwords} = dictionaryEntry; const definitionsChanged = this._removeArrayItemsWithDictionary(definitions, excludeDictionaryDefinitions); this._removeArrayItemsWithDictionary(pronunciations, excludeDictionaryDefinitions); this._removeArrayItemsWithDictionary(frequencies, excludeDictionaryDefinitions); @@ -794,12 +788,12 @@ export class Translator { * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry */ _removeUnusedHeadwords(dictionaryEntry) { - const { definitions, pronunciations, frequencies, headwords } = dictionaryEntry; + const {definitions, pronunciations, frequencies, headwords} = dictionaryEntry; const removeHeadwordIndices = new Set(); for (let i = 0, ii = headwords.length; i < ii; ++i) { removeHeadwordIndices.add(i); } - for (const { headwordIndices } of definitions) { + for (const {headwordIndices} of definitions) { for (const headwordIndex of headwordIndices) { removeHeadwordIndices.delete(headwordIndex); } @@ -831,7 +825,7 @@ export class Translator { * @param {Map} indexRemap */ _updateDefinitionHeadwordIndices(definitions, indexRemap) { - for (const { headwordIndices } of definitions) { + for (const {headwordIndices} of definitions) { for (let i = headwordIndices.length - 1; i >= 0; --i) { const newHeadwordIndex = indexRemap.get(headwordIndices[i]); if (typeof newHeadwordIndex === 'undefined') { @@ -850,7 +844,7 @@ export class Translator { _updateArrayItemsHeadwordIndex(array, indexRemap) { for (let i = array.length - 1; i >= 0; --i) { const item = array[i]; - const { headwordIndex } = item; + const {headwordIndex} = item; const newHeadwordIndex = indexRemap.get(headwordIndex); if (typeof newHeadwordIndex === 'undefined') { array.splice(i, 1); @@ -868,7 +862,7 @@ export class Translator { _removeArrayItemsWithDictionary(array, excludeDictionaryDefinitions) { let changed = false; for (let j = array.length - 1; j >= 0; --j) { - const { dictionary } = array[j]; + const {dictionary} = array[j]; if (!excludeDictionaryDefinitions.has(dictionary)) { continue; } array.splice(j, 1); changed = true; @@ -884,7 +878,7 @@ export class Translator { _removeArrayItemsWithDictionary2(array, excludeDictionaryDefinitions) { let changed = false; for (let j = array.length - 1; j >= 0; --j) { - const { dictionaries } = array[j]; + const {dictionaries} = array[j]; if (this._hasAny(excludeDictionaryDefinitions, dictionaries)) { continue; } array.splice(j, 1); changed = true; @@ -897,7 +891,7 @@ export class Translator { * @param {Set} excludeDictionaryDefinitions */ _removeTagGroupsWithDictionary(array, excludeDictionaryDefinitions) { - for (const { tags } of array) { + for (const {tags} of array) { this._removeArrayItemsWithDictionary2(tags, excludeDictionaryDefinitions); } } @@ -919,8 +913,8 @@ export class Translator { const allItems = []; /** @type {import('translator').TagTargetMap} */ const targetMap = new Map(); - for (const { tagGroups, tags } of tagTargets) { - for (const { dictionary, tagNames } of tagGroups) { + for (const {tagGroups, tags} of tagTargets) { + for (const {dictionary, tagNames} of tagGroups) { let dictionaryItems = targetMap.get(dictionary); if (typeof dictionaryItems === 'undefined') { dictionaryItems = new Map(); @@ -930,7 +924,7 @@ export class Translator { let item = dictionaryItems.get(tagName); if (typeof item === 'undefined') { const query = this._getNameBase(tagName); - item = { query, dictionary, tagName, cache: null, databaseTag: null, targets: [] }; + item = {query, dictionary, tagName, cache: null, databaseTag: null, targets: []}; dictionaryItems.set(tagName, item); allItems.push(item); } @@ -972,7 +966,7 @@ export class Translator { } } - for (const { dictionary, tagName, databaseTag, targets } of allItems) { + for (const {dictionary, tagName, databaseTag, targets} of allItems) { for (const tags of targets) { tags.push(this._createTag(databaseTag, tagName, dictionary)); } @@ -994,7 +988,7 @@ export class Translator { return i !== 0 ? i : stringComparer.compare(v1.name, v2.name); }; - for (const { tags } of tagTargets) { + for (const {tags} of tagTargets) { if (tags.length <= 1) { continue; } this._mergeSimilarTags(tags); tags.sort(compare); @@ -1008,7 +1002,7 @@ export class Translator { let tagCount = tags.length; for (let i = 0; i < tagCount; ++i) { const tag1 = tags[i]; - const { category, name } = tag1; + const {category, name} = tag1; for (let j = i + 1; j < tagCount; ++j) { const tag2 = tags[j]; if (tag2.name !== name || tag2.category !== category) { continue; } @@ -1049,7 +1043,7 @@ export class Translator { let lastPartOfSpeech = ''; const removeCategoriesSet = new Set(); - for (const { dictionary, tags } of definitions) { + for (const {dictionary, tags} of definitions) { const partOfSpeech = this._createMapKey(this._getTagNamesWithCategory(tags, 'partOfSpeech')); if (lastDictionary !== dictionary) { @@ -1086,9 +1080,9 @@ export class Translator { const headwordMapKeys = []; const headwordReadingMaps = []; - for (const { headwords, pronunciations, frequencies } of dictionaryEntries) { + for (const {headwords, pronunciations, frequencies} of dictionaryEntries) { for (let i = 0, ii = headwords.length; i < ii; ++i) { - const { term, reading } = headwords[i]; + const {term, reading} = headwords[i]; let readingMap = headwordMap.get(term); if (typeof readingMap === 'undefined') { readingMap = new Map(); @@ -1101,13 +1095,13 @@ export class Translator { targets = []; readingMap.set(reading, targets); } - targets.push({ headwordIndex: i, pronunciations, frequencies }); + targets.push({headwordIndex: i, pronunciations, frequencies}); } } const metas = await this._database.findTermMetaBulk(headwordMapKeys, enabledDictionaryMap); - for (const { mode, data, dictionary, index } of metas) { - const { index: dictionaryIndex, priority: dictionaryPriority } = this._getDictionaryOrder(dictionary, enabledDictionaryMap); + for (const {mode, data, dictionary, index} of metas) { + const {index: dictionaryIndex, priority: dictionaryPriority} = this._getDictionaryOrder(dictionary, enabledDictionaryMap); const map2 = headwordReadingMaps[index]; for (const [reading, targets] of map2.entries()) { switch (mode) { @@ -1116,8 +1110,8 @@ export class Translator { const hasReading = (data !== null && typeof data === 'object' && typeof data.reading === 'string'); if (hasReading && data.reading !== reading) { continue; } const frequency = hasReading ? data.frequency : /** @type {import('dictionary-data').GenericFrequencyData} */ (data); - for (const { frequencies, headwordIndex } of targets) { - const { frequency: frequencyValue, displayValue, displayValueParsed } = this._getFrequencyInfo(frequency); + for (const {frequencies, headwordIndex} of targets) { + const {frequency: frequencyValue, displayValue, displayValueParsed} = this._getFrequencyInfo(frequency); frequencies.push(this._createTermFrequency( frequencies.length, headwordIndex, @@ -1137,7 +1131,7 @@ export class Translator { if (data.reading !== reading) { continue; } /** @type {import('dictionary').TermPitch[]} */ const pitches = []; - for (const { position, tags, nasal, devoice } of data.pitches) { + for (const {position, tags, nasal, devoice} of data.pitches) { /** @type {import('dictionary').Tag[]} */ const tags2 = []; if (Array.isArray(tags)) { @@ -1145,9 +1139,9 @@ export class Translator { } const nasalPositions = this._toNumberArray(nasal); const devoicePositions = this._toNumberArray(devoice); - pitches.push({ position, nasalPositions, devoicePositions, tags: tags2 }); + pitches.push({position, nasalPositions, devoicePositions, tags: tags2}); } - for (const { pronunciations, headwordIndex } of targets) { + for (const {pronunciations, headwordIndex} of targets) { pronunciations.push(this._createTermPronunciation( pronunciations.length, headwordIndex, @@ -1170,18 +1164,18 @@ export class Translator { */ async _addKanjiMeta(dictionaryEntries, enabledDictionaryMap) { const kanjiList = []; - for (const { character } of dictionaryEntries) { + for (const {character} of dictionaryEntries) { kanjiList.push(character); } const metas = await this._database.findKanjiMetaBulk(kanjiList, enabledDictionaryMap); - for (const { character, mode, data, dictionary, index } of metas) { - const { index: dictionaryIndex, priority: dictionaryPriority } = this._getDictionaryOrder(dictionary, enabledDictionaryMap); + for (const {character, mode, data, dictionary, index} of metas) { + const {index: dictionaryIndex, priority: dictionaryPriority} = this._getDictionaryOrder(dictionary, enabledDictionaryMap); switch (mode) { case 'freq': { - const { frequencies } = dictionaryEntries[index]; - const { frequency, displayValue, displayValueParsed } = this._getFrequencyInfo(data); + const {frequencies} = dictionaryEntries[index]; + const {frequency, displayValue, displayValueParsed} = this._getFrequencyInfo(data); frequencies.push(this._createKanjiFrequency( frequencies.length, dictionary, @@ -1208,7 +1202,7 @@ export class Translator { const items = []; for (const [name] of statsEntries) { const query = this._getNameBase(name); - items.push({ query, dictionary }); + items.push({query, dictionary}); } const databaseInfos = await this._database.findTagMetaBulk(items); @@ -1220,7 +1214,7 @@ export class Translator { if (typeof databaseInfo === 'undefined') { continue; } const [name, value] = statsEntries[i]; - const { category } = databaseInfo; + const {category} = databaseInfo; let group = statsGroups.get(category); if (typeof group === 'undefined') { group = []; @@ -1271,7 +1265,7 @@ export class Translator { let displayValue = null; let displayValueParsed = false; if (typeof frequency === 'object' && frequency !== null) { - const { value: frequencyValue2, displayValue: displayValue2 } = frequency; + const {value: frequencyValue2, displayValue: displayValue2} = frequency; if (typeof frequencyValue2 === 'number') { frequencyValue = frequencyValue2; } if (typeof displayValue2 === 'string') { displayValue = displayValue2; } } else { @@ -1286,7 +1280,7 @@ export class Translator { break; } } - return { frequency: frequencyValue, displayValue, displayValueParsed }; + return {frequency: frequencyValue, displayValue, displayValueParsed}; } // Helpers @@ -1320,8 +1314,8 @@ export class Translator { */ _getDictionaryOrder(dictionary, enabledDictionaryMap) { const info = enabledDictionaryMap.get(dictionary); - const { index, priority } = typeof info !== 'undefined' ? info : { index: enabledDictionaryMap.size, priority: 0 }; - return { index, priority }; + const {index, priority} = typeof info !== 'undefined' ? info : {index: enabledDictionaryMap.size, priority: 0}; + return {index, priority}; } /** @@ -1375,7 +1369,7 @@ export class Translator { * @returns {import('dictionary').KanjiStat} */ _createKanjiStat(name, value, databaseInfo, dictionary) { - const { category, notes, order, score } = databaseInfo; + const {category, notes, order, score} = databaseInfo; return { name, category: (typeof category === 'string' && category.length > 0 ? category : 'default'), @@ -1399,7 +1393,7 @@ export class Translator { * @returns {import('dictionary').KanjiFrequency} */ _createKanjiFrequency(index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue, displayValueParsed) { - return { index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue, displayValueParsed }; + return {index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue, displayValueParsed}; } /** @@ -1436,7 +1430,7 @@ export class Translator { _createTag(databaseTag, name, dictionary) { let category, notes, order, score; if (typeof databaseTag === 'object' && databaseTag !== null) { - ({ category, notes, order, score } = databaseTag); + ({category, notes, order, score} = databaseTag); } return { name, @@ -1459,7 +1453,7 @@ export class Translator { * @returns {import('dictionary').TermSource} */ _createSource(originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary) { - return { originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary }; + return {originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary}; } /** @@ -1472,7 +1466,7 @@ export class Translator { * @returns {import('dictionary').TermHeadword} */ _createTermHeadword(index, term, reading, sources, tags, wordClasses) { - return { index, term, reading, sources, tags, wordClasses }; + return {index, term, reading, sources, tags, wordClasses}; } /** @@ -1516,7 +1510,7 @@ export class Translator { * @returns {import('dictionary').TermPronunciation} */ _createTermPronunciation(index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches) { - return { index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches }; + return {index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pitches}; } /** @@ -1532,7 +1526,7 @@ export class Translator { * @returns {import('dictionary').TermFrequency} */ _createTermFrequency(index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue, displayValueParsed) { - return { index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue, displayValueParsed }; + return {index, headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue, displayValueParsed}; } /** @@ -1577,9 +1571,9 @@ export class Translator { * @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 {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); + const {index: dictionaryIndex, priority: dictionaryPriority} = this._getDictionaryOrder(dictionary, enabledDictionaryMap); const sourceTermExactMatchCount = (isPrimary && deinflectedText === term ? 1 : 0); const source = this._createSource(originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary); const maxTransformedTextLength = transformedText.length; @@ -1619,7 +1613,7 @@ export class Translator { const headwords = new Map(); for (const dictionaryEntry of dictionaryEntries) { const headwordIndexMap = this._addTermHeadwords(headwords, dictionaryEntry.headwords, tagAggregator); - definitionEntries.push({ index: definitionEntries.length, dictionaryEntry, headwordIndexMap }); + definitionEntries.push({index: definitionEntries.length, dictionaryEntry, headwordIndexMap}); } // Sort @@ -1639,7 +1633,7 @@ export class Translator { const definitionsMap = checkDuplicateDefinitions ? new Map() : null; let inflections = null; - for (const { dictionaryEntry, headwordIndexMap } of definitionEntries) { + for (const {dictionaryEntry, headwordIndexMap} of definitionEntries) { score = Math.max(score, dictionaryEntry.score); dictionaryIndex = Math.min(dictionaryIndex, dictionaryEntry.dictionaryIndex); dictionaryPriority = Math.max(dictionaryPriority, dictionaryEntry.dictionaryPriority); @@ -1661,7 +1655,7 @@ export class Translator { const headwordsArray = [...headwords.values()]; let sourceTermExactMatchCount = 0; - for (const { sources } of headwordsArray) { + for (const {sources} of headwordsArray) { for (const source of sources) { if (source.isPrimary && source.matchSource === 'term') { ++sourceTermExactMatchCount; @@ -1709,7 +1703,7 @@ export class Translator { return; } for (const newSource of newSources) { - const { originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary } = newSource; + const {originalText, transformedText, deinflectedText, matchType, matchSource, isPrimary} = newSource; let has = false; for (const source of sources) { if ( @@ -1739,7 +1733,7 @@ export class Translator { _addTermHeadwords(headwordsMap, headwords, tagAggregator) { /** @type {number[]} */ const headwordIndexMap = []; - for (const { term, reading, sources, tags, wordClasses } of headwords) { + for (const {term, reading, sources, tags, wordClasses} of headwords) { const key = this._createMapKey([term, reading]); let headword = headwordsMap.get(key); if (typeof headword === 'undefined') { @@ -1787,7 +1781,7 @@ export class Translator { * @param {number[]} headwordIndexMap */ _addTermDefinitionsFast(definitions, newDefinitions, headwordIndexMap) { - for (const { headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, sequences, id, score, isPrimary, tags, entries } of newDefinitions) { + for (const {headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, sequences, id, score, isPrimary, tags, entries} of newDefinitions) { const headwordIndicesNew = []; for (const headwordIndex of headwordIndices) { headwordIndicesNew.push(headwordIndexMap[headwordIndex]); @@ -1804,7 +1798,7 @@ export class Translator { * @param {TranslatorTagAggregator} tagAggregator */ _addTermDefinitions(definitions, definitionsMap, newDefinitions, headwordIndexMap, tagAggregator) { - for (const { headwordIndices, dictionary, dictionaryIndex, dictionaryPriority, sequences, id, score, isPrimary, tags, entries } of newDefinitions) { + 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); if (typeof definition === 'undefined') { @@ -2006,7 +2000,7 @@ export class Translator { return i; }; - for (const { frequencies } of dictionaryEntries) { + for (const {frequencies} of dictionaryEntries) { frequencies.sort(compare); } } @@ -2019,12 +2013,12 @@ export class Translator { _updateSortFrequencies(dictionaryEntries, dictionary, ascending) { const frequencyMap = new Map(); for (const dictionaryEntry of dictionaryEntries) { - const { definitions, frequencies } = dictionaryEntry; + const {definitions, frequencies} = dictionaryEntry; let frequencyMin = Number.MAX_SAFE_INTEGER; let frequencyMax = Number.MIN_SAFE_INTEGER; for (const item of frequencies) { if (item.dictionary !== dictionary) { continue; } - const { headwordIndex, frequency } = item; + const {headwordIndex, frequency} = item; if (typeof frequency !== 'number') { continue; } frequencyMap.set(headwordIndex, frequency); frequencyMin = Math.min(frequencyMin, frequency); @@ -2038,7 +2032,7 @@ export class Translator { for (const definition of definitions) { frequencyMin = Number.MAX_SAFE_INTEGER; frequencyMax = Number.MIN_SAFE_INTEGER; - const { headwordIndices } = definition; + const {headwordIndices} = definition; for (const headwordIndex of headwordIndices) { const frequency = frequencyMap.get(headwordIndex); if (typeof frequency !== 'number') { continue; } @@ -2095,7 +2089,7 @@ class TranslatorTagAggregator { getTagExpansionTargets() { const results = []; for (const [tags, tagGroups] of this._tagExpansionTargetMap) { - results.push({ tags, tagGroups }); + results.push({tags, tagGroups}); } return results; } @@ -2108,7 +2102,7 @@ class TranslatorTagAggregator { const newTagGroups = this._tagExpansionTargetMap.get(newTags); if (typeof newTagGroups === 'undefined') { return; } const tagGroups = this._getOrCreateTagGroups(tags); - for (const { dictionary, tagNames } of newTagGroups) { + for (const {dictionary, tagNames} of newTagGroups) { const tagGroup = this._getOrCreateTagGroup(tagGroups, dictionary); this._addUniqueTags(tagGroup, tagNames); } @@ -2136,7 +2130,7 @@ class TranslatorTagAggregator { for (const tagGroup of tagGroups) { if (tagGroup.dictionary === dictionary) { return tagGroup; } } - const newTagGroup = { dictionary, tagNames: [] }; + const newTagGroup = {dictionary, tagNames: []}; tagGroups.push(newTagGroup); return newTagGroup; } @@ -2146,7 +2140,7 @@ class TranslatorTagAggregator { * @param {string[]} newTagNames */ _addUniqueTags(tagGroup, newTagNames) { - const { tagNames } = tagGroup; + const {tagNames} = tagGroup; for (const tagName of newTagNames) { if (tagNames.includes(tagName)) { continue; } tagNames.push(tagName); From 67af80e270ca571f559b6208b3ea554446ffe2b7 Mon Sep 17 00:00:00 2001 From: Cashew Date: Mon, 11 Dec 2023 00:09:02 +0900 Subject: [PATCH 05/12] Document dev/* --- dev/build-libs.js | 4 +++- dev/data-error.js | 3 +++ dev/dictionary-validate.js | 13 ++++++++----- dev/generate-css-json.js | 9 +++++---- dev/schema-validate.js | 1 + dev/translator-vm.js | 13 ++++++++++--- dev/util.js | 1 + 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/dev/build-libs.js b/dev/build-libs.js index 5caabec701..789849fc56 100644 --- a/dev/build-libs.js +++ b/dev/build-libs.js @@ -45,7 +45,9 @@ async function buildLib(scriptPath) { }); } -/** */ +/** + * Bundles libraries. + */ export async function buildLibs() { const devLibPath = path.join(dirname, 'lib'); const files = await fs.promises.readdir(devLibPath, { diff --git a/dev/data-error.js b/dev/data-error.js index 5034e3fd96..0ab2d35429 100644 --- a/dev/data-error.js +++ b/dev/data-error.js @@ -15,6 +15,9 @@ * along with this program. If not, see . */ +/** + * Schema validation error type. + */ class DataError extends Error { /** * @param {string} message diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js index a6948bfe6e..96195b74ef 100644 --- a/dev/dictionary-validate.js +++ b/dev/dictionary-validate.js @@ -71,9 +71,10 @@ async function validateDictionaryBanks(mode, zip, fileNameFormat, schema) { } /** - * @param {import('dev/schema-validate').ValidateMode} mode - * @param {import('jszip')} archive - * @param {import('dev/dictionary-validate').Schemas} schemas + * Validates a dictionary. + * @param {import('dev/schema-validate').ValidateMode} mode - Mode of validation. + * @param {import('jszip')} archive - Zip archive of the dictionary. + * @param {import('dev/dictionary-validate').Schemas} schemas - Schema to use for validation. */ export async function validateDictionary(mode, archive, schemas) { const fileName = 'index.json'; @@ -102,6 +103,7 @@ export async function validateDictionary(mode, archive, schemas) { } /** + * Returns a Schemas object from ext/data/schemas/*. * @returns {import('dev/dictionary-validate').Schemas} */ export function getSchemas() { @@ -118,8 +120,9 @@ export function getSchemas() { } /** - * @param {import('dev/schema-validate').ValidateMode} mode - * @param {string[]} dictionaryFileNames + * Validates dictionary files and logs the results to the console. + * @param {import('dev/schema-validate').ValidateMode} mode - Mode of validation. + * @param {string[]} dictionaryFileNames - Dictionary file names. */ export async function testDictionaryFiles(mode, dictionaryFileNames) { const schemas = getSchemas(); diff --git a/dev/generate-css-json.js b/dev/generate-css-json.js index e5d4d7f0ec..2b456a491d 100644 --- a/dev/generate-css-json.js +++ b/dev/generate-css-json.js @@ -86,11 +86,11 @@ function removeProperty(styles, property, removedProperties) { } /** - * @param {import('css-style-applier').RawStyleData} rules + * Manually formats JSON for improved compactness. + * @param {import('css-style-applier').RawStyleData} rules - CSS ruleset. * @returns {string} */ export function formatRulesJson(rules) { - // Manually format JSON, for improved compactness // return JSON.stringify(rules, null, 4); const indent1 = ' '; const indent2 = indent1.repeat(2); @@ -123,8 +123,9 @@ export function formatRulesJson(rules) { } /** - * @param {string} cssFile - * @param {string} overridesCssFile + * Generates a CSS ruleset. + * @param {string} cssFile - Path to CSS file. + * @param {string} overridesCssFile - Path to override CSS file. * @returns {import('css-style-applier').RawStyleData} * @throws {Error} */ diff --git a/dev/schema-validate.js b/dev/schema-validate.js index a1fe84552d..81953f496e 100644 --- a/dev/schema-validate.js +++ b/dev/schema-validate.js @@ -52,6 +52,7 @@ class JsonSchemaAjv { } /** + * Creates a JSON Schema. * @param {import('dev/schema-validate').ValidateMode} mode * @param {import('dev/schema-validate').Schema} schema * @returns {JsonSchema|JsonSchemaAjv} diff --git a/dev/translator-vm.js b/dev/translator-vm.js index 7fdda879eb..371fb67f34 100644 --- a/dev/translator-vm.js +++ b/dev/translator-vm.js @@ -32,6 +32,9 @@ vi.mock('../ext/js/language/dictionary-importer-media-loader.js'); const dirname = path.dirname(fileURLToPath(import.meta.url)); +/** + * Translator Virtual Machine. + */ export class TranslatorVM { constructor() { /** @type {import('dev/vm').PseudoChrome} */ @@ -55,15 +58,19 @@ export class TranslatorVM { this._dictionaryName = null; } - /** @type {Translator} */ + /** + * Returns this VM's translator. + * @type {Translator} + */ get translator() { if (this._translator === null) { throw new Error('Not prepared'); } return this._translator; } /** - * @param {string} dictionaryDirectory - * @param {string} dictionaryName + * Initialize this translator VM from a dictionary. + * @param {string} dictionaryDirectory - Directory of the dictionary files. + * @param {string} dictionaryName - Name of the dictionary. */ async prepare(dictionaryDirectory, dictionaryName) { // Dictionary diff --git a/dev/util.js b/dev/util.js index 3299dec4d8..67c992e63f 100644 --- a/dev/util.js +++ b/dev/util.js @@ -97,6 +97,7 @@ export function getAllFiles(baseDirectory, predicate=null) { } /** + * Creates a zip archive from the given dictionary directory. * @param {string} dictionaryDirectory * @param {string} [dictionaryName] * @returns {import('jszip')} From 3255e6d963281af3533dcf1e893df39032d29fec Mon Sep 17 00:00:00 2001 From: Cashew Date: Mon, 11 Dec 2023 01:13:15 +0900 Subject: [PATCH 06/12] add docs for deinflector.js --- ext/js/language/deinflector.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ext/js/language/deinflector.js b/ext/js/language/deinflector.js index b7a235d021..2747061ed0 100644 --- a/ext/js/language/deinflector.js +++ b/ext/js/language/deinflector.js @@ -16,9 +16,17 @@ * along with this program. If not, see . */ +/** + * This class deinflects Japanese terms to its dictionary form. + */ export class Deinflector { /** * @param {import('deinflector').ReasonsRaw} reasons + * @example + * const deinflectionReasons = JSON.parse( + * readFileSync(path.join('ext/data/deinflect.json')).toString(), + * ) as object; + * const deinflector = new Deinflector(deinflectionReasons); */ constructor(reasons) { /** @type {import('deinflector').Reason[]} */ @@ -26,8 +34,12 @@ export class Deinflector { } /** - * @param {string} source + * Deinflects a Japanese term to its dictionary form. + * @param {string} source - The source term to deinflect. * @returns {import('translation-internal').Deinflection[]} + * @example + * const deinflector = new Deinflector(deinflectionReasons); + * console.log(deinflector.deinflect('食べさせられる')); */ deinflect(source) { const results = [this._createDeinflection(source, 0, [])]; @@ -88,6 +100,7 @@ export class Deinflector { } /** + * Given a list of rules, return the corresponding deinflection rule flags. * @param {string[]} rules * @returns {import('translation-internal').DeinflectionRuleFlags} */ From 271e78fc4a768fbfbb1b3ae8a1c0b177dd8ee1df Mon Sep 17 00:00:00 2001 From: Cashew Date: Mon, 11 Dec 2023 03:09:24 +0900 Subject: [PATCH 07/12] update deinflector example --- ext/js/language/deinflector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ext/js/language/deinflector.js b/ext/js/language/deinflector.js index 2747061ed0..0362123492 100644 --- a/ext/js/language/deinflector.js +++ b/ext/js/language/deinflector.js @@ -34,11 +34,12 @@ export class Deinflector { } /** - * Deinflects a Japanese term to its dictionary form. + * Deinflects a Japanese term to all of its possible dictionary forms. * @param {string} source - The source term to deinflect. * @returns {import('translation-internal').Deinflection[]} * @example * const deinflector = new Deinflector(deinflectionReasons); + * // [{ term: '食べた', rules: 0, reasons: [] }, { term: '食べる', rules: 1, reasons: ['past'] }, { term: '食ぶ', rules: 2, reasons: ['potential', 'past'] }] * console.log(deinflector.deinflect('食べさせられる')); */ deinflect(source) { From fa209b47ef5ba74255bfa22779bfc61e844f1479 Mon Sep 17 00:00:00 2001 From: Cashew Date: Mon, 11 Dec 2023 04:19:30 +0900 Subject: [PATCH 08/12] Annotate --- ext/js/data/anki-note-builder.js | 29 ++++++++++++++++++++++++++ ext/js/data/database.js | 19 ++++++++++++++--- ext/js/language/dictionary-database.js | 28 ++++++++++++++++++------- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index 4920db390d..b9d6b7080c 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -22,9 +22,16 @@ import {TemplateRendererProxy} from '../templates/template-renderer-proxy.js'; import {yomitan} from '../yomitan.js'; import {AnkiUtil} from './anki-util.js'; +/** + * Anki Note Builder Class. + */ export class AnkiNoteBuilder { /** + * Initiate an instance of AnkiNoteBuilder. * @param {{japaneseUtil: import('../language/sandbox/japanese-util.js').JapaneseUtil}} details + * @example + * const japaneseUtil = new JapaneseUtil(null); + * const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil}); */ constructor({japaneseUtil}) { /** @type {import('../language/sandbox/japanese-util.js').JapaneseUtil} */ @@ -40,8 +47,30 @@ export class AnkiNoteBuilder { } /** + * Creates an Anki note. * @param {import('anki-note-builder').CreateNoteDetails} details * @returns {Promise} + * @example + * const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil}); + * const details = { + * dictionaryEntry, + * mode: 'test', + * context, + * template, + * deckName: 'deckName', + * modelName: 'modelName', + * fields, + * tags: ['yomitan'], + * checkForDuplicates: true, + * duplicateScope: 'collection', + * duplicateScopeCheckAllModels: false, + * resultOutputMode: mode, + * glossaryLayoutMode: 'default', + * compactTags: false, + * requirements: [], + * mediaOptions: null + * }; + * const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote(details); */ async createNote({ dictionaryEntry, diff --git a/ext/js/data/database.js b/ext/js/data/database.js index 026945caf4..557a625b84 100644 --- a/ext/js/data/database.js +++ b/ext/js/data/database.js @@ -17,6 +17,7 @@ */ /** + * Database class to store objects. * @template {string} TObjectStoreName */ export class Database { @@ -28,6 +29,7 @@ export class Database { } /** + * Opens the DB. * @param {string} databaseName * @param {number} version * @param {import('database').StructureDefinition[]} structure @@ -51,6 +53,7 @@ export class Database { } /** + * Closes the DB. * @throws {Error} */ close() { @@ -63,6 +66,7 @@ export class Database { } /** + * Returns true if DB opening is in process. * @returns {boolean} */ isOpening() { @@ -70,6 +74,7 @@ export class Database { } /** + * Returns true if the DB is open. * @returns {boolean} */ isOpen() { @@ -77,6 +82,7 @@ export class Database { } /** + * Returns a new transaction with the given mode ("readonly" or "readwrite") and scope which can be a single object store name or an array of names. * @param {string[]} storeNames * @param {IDBTransactionMode} mode * @returns {IDBTransaction} @@ -90,10 +96,12 @@ export class Database { } /** + * Add items in bulk to the object store. + * *count* items will be added beginning from *start* index of *items* list. * @param {TObjectStoreName} objectStoreName - * @param {unknown[]} items - * @param {number} start - * @param {number} count + * @param {unknown[]} items - List of items to add. + * @param {number} start - Start index. Added items begin at items[start]. + * @param {number} count - Count of items to add. * @returns {Promise} */ bulkAdd(objectStoreName, items, start, count) { @@ -244,6 +252,7 @@ export class Database { } /** + * Deletes records in store with the given key or in the given key range in query. * @param {TObjectStoreName} objectStoreName * @param {IDBValidKey|IDBKeyRange} key * @returns {Promise} @@ -258,6 +267,7 @@ export class Database { } /** + * Delete items in bulk from the object store. * @param {TObjectStoreName} objectStoreName * @param {?string} indexName * @param {IDBKeyRange} query @@ -291,6 +301,9 @@ export class Database { } /** + * Attempts to delete the named database. + * If the database already exists and there are open connections that don't close in response to a versionchange event, the request will be blocked until all they close. + * If the request is successful request's result will be null. * @param {string} databaseName * @returns {Promise} */ diff --git a/ext/js/language/dictionary-database.js b/ext/js/language/dictionary-database.js index c47e1e902f..1e15ff4214 100644 --- a/ext/js/language/dictionary-database.js +++ b/ext/js/language/dictionary-database.js @@ -19,6 +19,9 @@ import {log, stringReverse} from '../core.js'; import {Database} from '../data/database.js'; +/** + * This class represents the dictionary database. + */ export class DictionaryDatabase { constructor() { /** @type {Database} */ @@ -141,6 +144,7 @@ export class DictionaryDatabase { } /** + * Purges the database. * @returns {Promise} */ async purge() { @@ -162,6 +166,7 @@ export class DictionaryDatabase { } /** + * Deletes a dictionary. * @param {string} dictionaryName * @param {number} progressRate * @param {import('dictionary-database').DeleteDictionaryProgressCallback} onProgress @@ -225,9 +230,10 @@ export class DictionaryDatabase { } /** - * @param {string[]} termList - * @param {import('dictionary-database').DictionarySet} dictionaries - * @param {import('dictionary-database').MatchType} matchType + * Find terms in bulk. + * @param {string[]} termList - The list of terms to find. + * @param {import('dictionary-database').DictionarySet} dictionaries - Dictionaries to find the terms from. + * @param {import('dictionary-database').MatchType} matchType - Matching type. * @returns {Promise} */ findTermsBulk(termList, dictionaries, matchType) { @@ -259,8 +265,9 @@ export class DictionaryDatabase { } /** - * @param {import('dictionary-database').TermExactRequest[]} termList - * @param {import('dictionary-database').DictionarySet} dictionaries + * Find exact terms in bulk. + * @param {import('dictionary-database').TermExactRequest[]} termList - The list of terms to find. + * @param {import('dictionary-database').DictionarySet} dictionaries - Dictionaries to find the term from. * @returns {Promise} */ findTermsExactBulk(termList, dictionaries) { @@ -270,6 +277,7 @@ export class DictionaryDatabase { } /** + * Find terms by sequence in bulk. * @param {import('dictionary-database').DictionaryAndQueryRequest[]} items * @returns {Promise} */ @@ -280,6 +288,7 @@ export class DictionaryDatabase { } /** + * Find term meta in bulk. * @param {string[]} termList * @param {import('dictionary-database').DictionarySet} dictionaries * @returns {Promise} @@ -291,8 +300,9 @@ export class DictionaryDatabase { } /** - * @param {string[]} kanjiList - * @param {import('dictionary-database').DictionarySet} dictionaries + * Find kanji in bulk. + * @param {string[]} kanjiList - The list of kanji to find. + * @param {import('dictionary-database').DictionarySet} dictionaries - Dictionaries to find from. * @returns {Promise} */ findKanjiBulk(kanjiList, dictionaries) { @@ -302,6 +312,7 @@ export class DictionaryDatabase { } /** + * Find kanji meta in bulk. * @param {string[]} kanjiList * @param {import('dictionary-database').DictionarySet} dictionaries * @returns {Promise} @@ -313,6 +324,7 @@ export class DictionaryDatabase { } /** + * Find tag meta in bulk. * @param {import('dictionary-database').DictionaryAndQueryRequest[]} items * @returns {Promise<(import('dictionary-database').Tag|undefined)[]>} */ @@ -323,6 +335,7 @@ export class DictionaryDatabase { } /** + * Find tag for title. * @param {string} name * @param {string} dictionary * @returns {Promise} @@ -343,6 +356,7 @@ export class DictionaryDatabase { } /** + * Get dictionary metadata. * @returns {Promise} */ getDictionaryInfo() { From 56de831c719e6aeb04f139197b3d873d27537615 Mon Sep 17 00:00:00 2001 From: Cashew Date: Mon, 11 Dec 2023 04:29:41 +0900 Subject: [PATCH 09/12] Revert "Merge branch 'development' of https://github.com/Scrub1492/lesen-tan into development" This reverts commit b92348f702bc031b36f24462adfa940d17f9ecdd, reversing changes made to 3255e6d963281af3533dcf1e893df39032d29fec. --- README.md | 422 ++++++++++++++- ext/css/display.css | 140 ----- ext/js/background/backend.js | 692 ++++++++++++------------- ext/js/display/display.js | 237 ++------- ext/js/language/dictionary-database.js | 61 ++- ext/js/language/text-scanner.js | 154 ++---- ext/js/language/translator.js | 198 +------ ext/search.html | 3 +- types/ext/dictionary-database.d.ts | 2 - types/ext/translation-internal.d.ts | 2 - 10 files changed, 880 insertions(+), 1031 deletions(-) diff --git a/README.md b/README.md index 53427e0a85..3fbf67c84d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,425 @@ -# Lesen-tan +# Yomitan + +[![Chrome Release (Stable)]()](https://chrome.google.com/webstore/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn) [![Firefox Release (Stable)]()](https://addons.mozilla.org/en-US/firefox/addon/yomitan/) +[![Chrome Release (Testing)]()](https://chrome.google.com/webstore/detail/yomitan-development-build/glnaenfapkkecknnmginabpmgkenenml) [![Firefox Release (Testing)]()](https://github.com/themoeway/yomitan/releases) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/themoeway/yomitan/badge)](https://securityscorecards.dev/viewer/?uri=github.com/themoeway/yomitan) [![Discord Server](https://dcbadge.vercel.app/api/server/UGNPMDE7zC?style=flat)](https://discord.gg/UGNPMDE7zC) ## Project Introduction -:wave: **A hover dictionary for conjugated languages (German, French, Spanish)** +:wave: **This project is a community fork of Yomichan** (which was [sunset](https://foosoft.net/posts/sunsetting-the-yomichan-project/) by its owner on Feb 26 2023). + +We have made a number of foundational changes to ensure **the project stays alive, works on latest browser versions, and is easy to contribute to**: +* Completed the Manifest V2 → Manifest V3 transition, which is required to submit a new extension to the Chrome webstore. It will also be long-term required for usage of the extension, as [Manifest V2 extensions will start being disabled as early as June 2024](https://developer.chrome.com/blog/resuming-the-transition-to-mv3/). +* Switched to using ECMAScript modules and npm-sourced dependencies to make for a more modern coding and packaging experience. +* Implemented an end-to-end CI/CD pipeline to make it easy to rapidly iterate and deploy new versions. +* Switched to standard testing frameworks, vitest and playwright, to make it easier to develop more comprehensive tests, and detect regressions. + +In addition, we are beginning to make important bug fixes and minor enhancements: +* Improve dictionary import speed by 2x~10x or more (depending on the dictionary) +* Fix UI regressions on modern browser versions, like [the popup being too small](https://github.com/themoeway/yomitan/pull/228) +* Add functionality to import/export multiple dictionaries, to make your data more portable across machines +* And [more](https://github.com/themoeway/yomitan/pulls?q=is%3Apr+is%3Amerged+-label%3Aarea%2Fdependencies+-label%3Akind%2Fmeta) + +Since the owner requested forks be uniquely named, we have chosen a new name, _yomitan_. (_-tan_ is an honorific used for anthropomorphic moe characters.) While we've made some substantial changes, the majority of the extension's functionality is thanks to hard work of foosoft and numerous other open source contributors from 2016-2023. + +Since this is a distributed effort, we **highly welcome new contributors**! Feel free to browse the issue tracker, and you can find us on [TheMoeWay Discord](https://discord.gg/UGNPMDE7zC) at [#yomitan-development](https://discord.com/channels/617136488840429598/1081538711742844980). + +## Tool Introduction +Yomitan turns your web browser into a tool for building Japanese language literacy by helping you to decipher texts +which would be otherwise too difficult tackle. This extension is similar to +[10ten Japanese Reader (formerly Rikaichamp)](https://addons.mozilla.org/en-US/firefox/addon/10ten-ja-reader/) for Firefox and +[Rikaikun](https://chrome.google.com/webstore/detail/rikaikun/jipdnfibhldikgcjhfnomkfpcebammhp?hl=en) for Chrome, but it +stands apart in its goal of being an all-encompassing learning tool as opposed to a mere browser-based dictionary. + +Yomitan provides advanced features not available in other browser-based dictionaries: -This is a fork of the amazing [Yomitan](https://github.com/themoeway/yomitan) project for Japanese, and is based on [yomichan-de](https://github.com/seth-js/yomichan-de). Please check out their github repos for more detailed information about the projects themselves. +- Interactive popup definition window for displaying search results. +- On-demand audio playback for select dictionary definitions. +- Kanji stroke order diagrams are just a click away for most characters. +- Custom search page for easily executing custom search queries. +- Support for multiple dictionary formats including [EPWING](https://ja.wikipedia.org/wiki/EPWING) via the [Yomitan Import](https://github.com/themoeway/yomitan-import) tool. +- Automatic note creation for the [Anki](https://apps.ankiweb.net/) flashcard program via the [AnkiConnect](https://foosoft.net/projects/anki-connect) plugin. +- Clean, modern code makes it easy for developers to [contribute](https://github.com/themoeway/yomitan/blob/master/CONTRIBUTING.md) new features. -If you find any issues or simply want to contribute, please open an issue via the [issue tracker](https://github.com/Scrub1492/lesen-tan/issues) or find my on Discord under the name cashewnuttynuts. +[![Term definitions](img/ss-terms-thumb.png)](img/ss-terms.png) +[![Kanji information](img/ss-kanji-thumb.png)](img/ss-kanji.png) +[![Dictionary options](img/ss-dictionaries-thumb.png)](img/ss-dictionaries.png) +[![Anki options](img/ss-anki-thumb.png)](img/ss-anki.png) -**Currently Lesen-tan will work with only German. It will currently have unintended results when performing look-ups in other languages (including Japanese). +## Table of Contents + +- [Installation](#installation) +- [Migrating from Yomichan](#migrating-from-yomichan) + - [Exporting Data](#exporting-data) + - [Custom Templates](#custom-templates) +- [Dictionaries](#dictionaries) +- [Basic Usage](#basic-usage) + - [Importing Dictionaries](#importing-dictionaries) + - [Importing and Exporting Personal Configuration](#importing-and-exporting-personal-configuration) +- [Custom Dictionaries](#custom-dictionaries) +- [Anki Integration](#anki-integration) + - [Flashcard Configuration](#flashcard-configuration) + - [Flashcard Creation](#flashcard-creation) +- [Keyboard Shortcuts](#keyboard-shortcuts) +- [Advanced Options](#advanced-options) + - [Parse sentences using MeCab](#parse-sentences-using-mecab) +- [Frequently Asked Questions](#frequently-asked-questions) +- [Licenses](#licenses) +- [Third-Party Libraries](#third-party-libraries) ## Installation -Follow the steps in [yomichan-de](https://github.com/seth-js/yomichan-de). +Yomitan comes in two flavors: _stable_ and _testing_. Over the years, this extension has evolved to contain many +complex features which have become increasingly difficult to test across different browsers, versions, and environments. +New changes are initially introduced into the _testing_ version, and after some time spent ensuring that they are +relatively bug free, they will be promoted to the _stable_ version. If you are technically savvy and don't mind +submitting issues on GitHub, try the _testing_ version; otherwise, the _stable_ version will be your best bet. + +- **Google Chrome** + + - [stable](https://chrome.google.com/webstore/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn) + - [testing](https://chrome.google.com/webstore/detail/yomitan-development-build/glnaenfapkkecknnmginabpmgkenenml) + +- **Mozilla Firefox** + - [stable](https://addons.mozilla.org/en-US/firefox/addon/yomitan/) + - [testing](https://github.com/themoeway/yomitan/releases) ※ + +※ NOTE: Unlike Chrome, Firefox does not allow extensions meant for testing to be hosted in the marketplace. +You will have to download a desired version and side-load it yourself. You only need to do this once and will get +updates automatically. + +## Migrating from Yomichan + +### Exporting Data + +If you are an existing user of Yomichan, you can export your dictionary collection and settings such that they can be imported into Yomitan to reflect your setup exactly as it was. + +You can export your settings from Yomichan's Settings page. Go to the `Backup` section and click on `Export Settings`. + +Yomichan doesn't have first-class support to export the dictionary collection. Please follow the instructions provided in the following link to export your data: +https://github.com/themoeway/yomichan-data-exporter#steps-to-export-the-data + +You can then import the exported files into Yomitan from the `Backup` section of the `Settings` page. Please see [the section on importing dictionaries](#importing-dictionaries) further below for more explicit steps. + +### Custom Templates + +If you do not use custom templates for Anki note creation, this section can be skipped. + +Due to security concerns, an alternate implementation of Handlebars is being used which behaves slightly differently. +This revealed a bug in four of Yomitan's template helpers, which have now been fixed in the default templates. If your +custom templates use the following helpers, please ensure their use matches the corrected forms. + +| Helper | Example | Corrected | +| ---------------- | ------------------------------------------------------------- | ------------------------------------ | +| `formatGlossary` | `{{#formatGlossary ../dictionary}}{{{.}}}{{/formatGlossary}}` | `{{formatGlossary ../dictionary .}}` | +| `furigana` | `{{#furigana}}{{{definition}}}{{/furigana}}` | `{{furigana definition}}` | +| `furiganaPlain` | `{{~#furiganaPlain}}{{{.}}}{{/furiganaPlain~}}` | `{{~furiganaPlain .~}}` | +| `dumpObject` | `{{#dumpObject}}{{{.}}}{{/dumpObject}}` | `{{dumpObject .}}` | + +Authors of custom templates may be interested to know that other helpers previously used and documented in the block +form (e.g. `{{#set "key" "value"}}{{/set}}`), while not broken by this change, may also be replaced with the less verbose +form (e.g. `{{set "key" "value"}}`). The default templates and helper documentation have been changed to reflect this. + +## Dictionaries + +There are several free Japanese dictionaries available for Yomitan, with two of them having glossaries available in +different languages. You must download and import the dictionaries you wish to use in order to enable Yomitan +definition lookups. If you have proprietary EPWING dictionaries that you would like to use, check the [Yomitan +Import](https://github.com/themoeway/yomitan-import) page to learn how to convert and import them into Yomitan. + +Be aware that non-English dictionaries contain fewer entries than their English counterparts. Even if your primary +language is not English, you may consider also importing the English version for better coverage. + +- [Jitendex](https://github.com/stephenmk/Jitendex) - Jitendex is an improved version of JMdict for Yomitan. It features better formatting and some other improvements, and is actively being improved by its author. +- [JMdict](https://github.com/themoeway/jmdict-yomitan#jmdict-for-yomitan-1) - There are daily automatically updated builds of JMdict for Yomitan available in this repository. It is available in multiple languages and formats, but we recommend installing the more modern Jitendex for English users. +- [JMnedict](https://github.com/themoeway/jmdict-yomitan#jmnedict-for-yomitan) - JMnedict is a dictionary that lists readings of person/place/organization names and other proper nouns. +- [KANJIDIC](https://github.com/themoeway/jmdict-yomitan#kanjidic-for-yomitan) - KANJIDIC is an English dictionary listing readings, meanings, and other info about kanji characters. + +## Basic Usage + +1. Click the _Yomitan_ button in the browser bar to open the quick-actions popup. + + + + - The _cog_ button will open the Settings page. + - The _magnifying glass_ button will open the Search page. + - The _question mark_ button will open the Information page. + - The _profile_ button will appear when multiple profiles exist, allowing the current profile to be quickly changed. + +2. Import the dictionaries you wish to use for term and kanji searches. If you do not have any dictionaries installed + or enabled, Yomitan will warn you that it is not ready for use by displaying an orange exclamation mark over its + icon. This exclamation mark will disappear once you have installed and enabled at least one dictionary. + + + +3. Webpage text can be scanned by moving the cursor while holding a modifier key, which is Shift + by default. If definitions are found for the text at the cursor position, a popup window containing term definitions + will open. This window can be dismissed by clicking anywhere outside of it. + + + +4. Click on the _speaker_ button to hear the term pronounced by a native speaker. If an audio sample is + not available, you will hear a short click instead. You can configure the sources used to retrieve audio samples in + the options page. + +5. Click on individual kanji in the term definition results to view additional information about those characters, + including stroke order diagrams, readings, meanings, as well as other useful data. + + + +### Importing Dictionaries + +You can import individual dictionaries from the settings page as described above. + +Yomitan also supports exporting and importing your entire collection of dictionaries. + +#### Importing a Dictionary Collection + +- Go to Yomitan's Settings page (Click on the extension's icon then click on the cog icon from the popup) +- Click `Import Dictionary Collection` and select the database file you want to import +- Wait for the import to finish then turn all the dictionaries back on from the `Dictionaries > Configure installed and enabled dictionaries` section +- Refresh the browser tab to see the dictionaries in effect + +#### Exporting the Dictionary Collection + +- Click `Export Dictionary Collection` from the backup section of Yomitan's settings page +- It will show you a progress report as it exports the data then initiates a + download for a file named something like `yomitan-dictionaries-YYYY-MM-DD-HH-mm-ss.json` + (e.g. `yomitan-dictionaries-2023-07-05-02-42-04.json`) + +### Importing and Exporting Personal Configuration + +Note that you can also similarly export and import your Yomitan settings from the `Backup` section of the Settings page. + +You should be able to replicate your exact Yomitan setup across devices by exporting your settings and dictionary collection from the source device then importing those from the destination. + +## Custom Dictionaries + +Yomitan supports the use of custom dictionaries, including the esoteric but popular +[EPWING](https://ja.wikipedia.org/wiki/EPWING) format. They were often utilized in portable electronic dictionaries +similar to the ones pictured below. These dictionaries are often sought after by language learners for their correctness +and excellent coverage of the Japanese language. + +Unfortunately, as most of the dictionaries released in this format are proprietary, they are unable to be bundled with +Yomitan. Instead, you will need to procure these dictionaries yourself and import them using [Yomitan +Import](https://github.com/themoeway/yomitan-import). Check the project page for additional details. + +![Pocket EPWING dictionaries](img/epwing-devices.jpg) + +## Anki Integration + +Yomitan features automatic flashcard creation for [Anki](https://apps.ankiweb.net/), a free application designed to help you +retain knowledge. This feature requires the prior installation of an Anki plugin called [AnkiConnect](https://foosoft.net/projects/anki-connect). +Check the respective project page for more information about how to set up this software. + +### Flashcard Configuration + +Before flashcards can be automatically created, you must configure the templates used to create term and/or kanji notes. +If you are unfamiliar with Anki deck and model management, this would be a good time to reference the [Anki +Manual](https://docs.ankiweb.net/#/). In short, you must specify what information should be included in the +flashcards that Yomitan creates through AnkiConnect. + +Flashcard fields can be configured with the following steps: + +1. Open the Yomitan options page and scroll down to the section labeled _Anki Options_. +2. Tick the checkbox labeled _Enable Anki integration_ (Anki must be running with [AnkiConnect](https://foosoft.net/projects/anki-connect) installed). +3. Select the type of template to configure by clicking on either the _Terms_ or _Kanji_ tabs. +4. Select the Anki deck and model to use for new creating new flashcards of this type. +5. Fill the model fields with markers corresponding to the information you wish to include (several can be used at + once). Advanced users can also configure the actual [Handlebars](https://handlebarsjs.com/) templates used to create + the flashcard contents (this is strictly optional). + + #### Markers for Term Cards + + | Marker | Description | + | -------------------------- | ------------------------------------------------------------------------------------------------------------------------ | + | `{audio}` | Audio sample of a native speaker's pronunciation in MP3 format (if available). | + | `{clipboard-image}` | An image which is stored in the system clipboard, if present. | + | `{clipboard-text}` | Text which is stored in the system clipboard, if present. | + | `{cloze-body}` | Raw, inflected term as it appeared before being reduced to dictionary form by Yomitan. | + | `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. | + | `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. | + | `{conjugation}` | Conjugation path from the raw inflected term to the source term. | + | `{dictionary}` | Name of the dictionary from which the card is being created (unavailable in _grouped_ mode). | + | `{document-title}` | Title of the web page that the term appeared in. | + | `{expression}` | Term expressed as kanji (will be displayed in kana if kanji is not available). | + | `{frequencies}` | Frequency information for the term. | + | `{furigana}` | Term expressed as kanji with furigana displayed above it (e.g. 日本語にほんご). | + | `{furigana-plain}` | Term expressed as kanji with furigana displayed next to it in brackets (e.g. 日本語[にほんご]). | + | `{glossary}` | List of definitions for the term (output format depends on whether running in _grouped_ mode). | + | `{glossary-brief}` | List of definitions for the term in a more compact format. | + | `{glossary-no-dictionary}` | List of definitions for the term, except the dictionary tag is omitted. | + | `{part-of-speech}` | Part of speech information for the term. | + | `{pitch-accents}` | List of pitch accent downstep notations for the term. | + | `{pitch-accent-graphs}` | List of pitch accent graphs for the term. | + | `{pitch-accent-positions}` | List of accent downstep positions for the term as a number. | + | `{reading}` | Kana reading for the term (empty for terms where the expression is the reading). | + | `{screenshot}` | Screenshot of the web page taken at the time the term was added. | + | `{search-query}` | The full search query shown on the search page. | + | `{selection-text}` | The selected text on the search page or popup. | + | `{sentence}` | Sentence, quote, or phrase that the term appears in from the source content. | + | `{sentence-furigana}` | Sentence, quote, or phrase that the term appears in from the source content, with furigana added. | + | `{tags}` | Grammar and usage tags providing information about the term (unavailable in _grouped_ mode). | + | `{url}` | Address of the web page in which the term appeared in. | + + #### Markers for Kanji Cards + + | Marker | Description | + | --------------------- | ------------------------------------------------------------------------------------------------------------------------ | + | `{character}` | Unicode glyph representing the current kanji. | + | `{clipboard-image}` | An image which is stored in the system clipboard, if present. | + | `{clipboard-text}` | Text which is stored in the system clipboard, if present. | + | `{cloze-body}` | Raw, inflected parent term as it appeared before being reduced to dictionary form by Yomitan. | + | `{cloze-prefix}` | Fragment of the containing `{sentence}` starting at the beginning of `{sentence}` until the beginning of `{cloze-body}`. | + | `{cloze-suffix}` | Fragment of the containing `{sentence}` starting at the end of `{cloze-body}` until the end of `{sentence}`. | + | `{dictionary}` | Name of the dictionary from which the card is being created. | + | `{document-title}` | Title of the web page that the kanji appeared in. | + | `{frequencies}` | Frequency information for the kanji. | + | `{glossary}` | List of definitions for the kanji. | + | `{kunyomi}` | Kunyomi (Japanese reading) for the kanji expressed as katakana. | + | `{onyomi}` | Onyomi (Chinese reading) for the kanji expressed as hiragana. | + | `{screenshot}` | Screenshot of the web page taken at the time the kanji was added. | + | `{search-query}` | The full search query shown on the search page. | + | `{selection-text}` | The selected text on the search page or popup. | + | `{sentence}` | Sentence, quote, or phrase that the character appears in from the source content. | + | `{sentence-furigana}` | Sentence, quote, or phrase that the character appears in from the source content, with furigana added. | + | `{stroke-count}` | Number of strokes that the kanji character has. | + | `{url}` | Address of the web page in which the kanji appeared in. | + +When creating your model for Yomitan, _make sure that you pick a unique field to be first_; fields that will +contain `{expression}` or `{character}` are ideal candidates for this. Anki does not allow duplicate flashcards to be +added to a deck by default; it uses the first field in the model to check for duplicates. For example, if you have `{reading}` +configured to be the first field in your model and はし is already in your deck, you will not +be able to create a flashcard for はし because they share the same reading. + +### Flashcard Creation + +Once Yomitan is configured, it becomes trivial to create new flashcards with a single click. You will see the following +icons next to term definitions: + +- Clicking ![](img/btn-add-expression.png) adds the current expression as kanji (e.g. 食べる). +- Clicking ![](img/btn-add-reading.png) adds the current expression as hiragana or katakana (e.g. たべる). + +Below are some troubleshooting tips you can try if you are unable to create new flashcards: + +- Individual icons will appear grayed out if a flashcard cannot be created for the current definition (e.g. it already exists in the deck). +- If all of the buttons appear grayed out, then you should double-check your deck and model configuration settings. +- If no icons appear at all, make sure that Anki is running in the background and that [AnkiConnect](https://foosoft.net/projects/anki-connect) has been installed. + +## Keyboard Shortcuts + +The following shortcuts are globally available: + +| Shortcut | Action | +| ---------------------------------- | ------------------------ | +| Alt + Insert | Open search page. | +| Alt + Delete | Toggle extension on/off. | + +The following shortcuts are available on search results: + +| Shortcut | Action | +| -------------------------------- | --------------------------------------- | +| Esc | Cancel current search. | +| Alt + PgUp | Page up through results. | +| Alt + PgDn | Page down through results. | +| Alt + End | Go to last result. | +| Alt + Home | Go to first result. | +| Alt + Up | Go to previous result. | +| Alt + Down | Go to next result. | +| Alt + b | Go to back to source term. | +| Alt + e | Add current term as expression to Anki. | +| Alt + r | Add current term as reading to Anki. | +| Alt + p | Play audio for current term. | +| Alt + k | Add current kanji to Anki. | + +## Advanced Options + +Click the `Advanced` toggle switch in the bottom left corner of the Settings page to enable advanced options. + +### Parse sentences using MeCab + +[MeCab](https://taku910.github.io/mecab/) is a third-party program which uses its own dictionaries and parsing algorithm to decompose sentences into individual words. MeCab may provide more accurate parsing results than Yomitan's internal parser. + +In order for Yomitan to use it, both MeCab and a native messaging component must be installed. +A setup guide can be found [here](https://github.com/themoeway/yomitan-mecab-installer/blob/master/README.md). + +## Frequently Asked Questions + +**I'm having problems importing dictionaries in Firefox, what do I do?** + +Yomitan uses the cross-browser IndexedDB system for storing imported dictionary data into your user profile. Although +everything "just works" in Chrome, depending on settings, Firefox users can run into problems due to browser bugs. +Yomitan catches errors and tries to offer suggestions about how to work around Firefox issues, but in general at least +one of the following solutions should work for you: + +- Make sure you have cookies enabled. It appears that disabling them also disables IndexedDB for some reason. You + can still have cookies be disabled on other sites; just make sure to add the Yomitan extension to the whitelist of + whatever tool you are using to restrict cookies. You can get the extension "URL" by looking at the address bar when + you have the search page open. +- Make sure that you have sufficient disk space available on the drive Firefox uses to store your user profile. + Firefox limits the amount of space that can be used by IndexedDB to a small fraction of the disk space actually + available on your computer. +- Make sure that you have history set to "Remember history" enabled in your privacy settings. When this option is + set to "Never remember history", IndexedDB access is once again disabled for an inexplicable reason. +- As a last resort, try using the [Refresh Firefox](https://support.mozilla.org/en-US/kb/reset-preferences-fix-problems) + feature to reset your user profile. It appears that the Firefox profile system can corrupt itself preventing + IndexedDB from being accessible to Yomitan. + +**Will you add support for online dictionaries?** + +Online dictionaries will not be implemented because it is not possible to support them in a robust way. In order to +perform Japanese deinflection, Yomitan must execute dozens of database queries for every single word. Factoring in +network latency and the fragility of web scraping, it would not be possible to maintain a good and consistent user +experience. + +**Is it possible to use Yomitan with files saved locally on my computer with Chrome?** + +In order to use Yomitan with local files in Chrome, you must first tick the _Allow access to file URLs_ checkbox +for Yomitan on the extensions page. Due to the restrictions placed on browser addons in the WebExtensions model, it +will likely never be possible to use Yomitan with PDF files. + +**Is it possible to delete individual dictionaries without purging the database?** + +Yomitan is able to delete individual dictionaries, but keep in mind that this process can be _very_ slow and can +cause the browser to become unresponsive. The time it takes to delete a single dictionary can sometimes be roughly +the same as the time it originally took to import, which can be significant for certain large dictionaries. + +**Why aren't EPWING dictionaries bundled with Yomitan?** + +The vast majority of EPWING dictionaries are proprietary, so they are unfortunately not able to be included in +this extension due to copyright reasons. + +**When are you going to add support for $MYLANGUAGE?** + +Developing Yomitan requires a decent understanding of Japanese sentence structure and grammar, and other languages +are likely to have their own unique set of rules for syntax, grammar, inflection, and so on. Supporting additional +languages would not only require many additional changes to the codebase, it would also incur significant maintenance +overhead and knowledge demands for the developers. Therefore, suggestions and contributions for supporting +new languages will be declined, allowing Yomitan's focus to remain Japanese-centric. + +## Licenses + +Required licensing notices for this project follow below: + +- **EDRDG License** \ + This package uses the [EDICT](https://www.edrdg.org/jmdict/edict.html) and + [KANJIDIC](https://www.edrdg.org/wiki/index.php/KANJIDIC_Project) dictionary files. These files are the property of + the [Electronic Dictionary Research and Development Group](https://www.edrdg.org/), and are used in conformance with + the Group's [license](https://www.edrdg.org/edrdg/licence.html). + +- **Kanjium License** \ + The pitch accent notation, verb particle data, phonetics, homonyms and other additions or modifications to EDICT, + KANJIDIC or KRADFILE were provided by Uros Ozvatic through his free database. + +## Third-Party Libraries + +Yomitan uses several third-party libraries to function. + +| Name | Installed version | License type | Link | +| :------------------ | :---------------- | :----------- | :------------------------------------------------------- | +| @zip.js/zip.js | 2.7.31 | BSD-3-Clause | git+https://github.com/gildas-lormeau/zip.js.git | +| dexie | 3.2.4 | Apache-2.0 | git+https://github.com/dfahlander/Dexie.js.git | +| dexie-export-import | 4.0.7 | Apache-2.0 | git+https://github.com/dexie/Dexie.js.git | +| handlebars | 4.7.8 | MIT | git+https://github.com/handlebars-lang/handlebars.js.git | +| parse5 | 7.1.2 | MIT | git://github.com/inikulin/parse5.git | +| wanakana | 5.3.1 | MIT | git+ssh://git@github.com/WaniKani/WanaKana.git | diff --git a/ext/css/display.css b/ext/css/display.css index f29bb943b0..49aeaaa5d0 100644 --- a/ext/css/display.css +++ b/ext/css/display.css @@ -1,5 +1,4 @@ /* - * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2016-2022 Yomichan Authors * @@ -1953,142 +1952,3 @@ button.footer-notification-close-button { :root[data-popup-display-mode=full-width] .frame-resizer-container { display: none; } - -/* https://github.com/seth-js/yomichan-de */ -/* Seth's Custom CSS */ -.gloss-list { - list-style-type: decimal; -} - -.tag[data-category='dictionary'] { - display: none; -} - -.tag[data-category='masculine'] { - --tag-color: #0070ff; -} - -.tag[data-category='feminine'] { - --tag-color: #ec3654; -} - -.tag[data-category='neuter'] { - --tag-color: #269100; -} - -.definition-tag-list { - display: inline; -} - -.inflection { - color: var(--reason-text-color); - font-size: 12px; - font-style: italic; -} - -.inflection-list { - display: none; -} - -.inflection-separator+.inflection::before { - content: var(--inflection-separator); - padding: 0 0.25em; -} - -div[data-section-type='frequencies'] { - display: none; -} - -:root[data-theme='dark'] { - --text-color: white; -} - -.headword-term { - font-weight: 300; -} - -.entry { - padding: 30px 0; -} - -*::-webkit-scrollbar { - width: 5px !important; -} - -*::-webkit-scrollbar-thumb { - /* background: #777 !important; */ - border-radius: 20px !important; -} - -* { - /* scrollbar-color: #777 #000 !important; */ - scrollbar-width: thin !important; -} - -.gloss-list { - margin-top: 10px; -} - -.gloss-item { - margin-bottom: 7px; -} - -.entry-header { - margin-bottom: 10px; -} - -.form-info-box { - display: none; - padding: 20px; - width: max-content; - box-shadow: 0 1px 12px rgb(0 0 0 / 12%), 0 1px 4px rgb(0 0 0 / 24%); - z-index: 1; - margin-top: 10px; -} - -:root[data-theme="dark"] .form-info-box { - border: 1px solid #333; - background-color: #1e1e1e; -} - -.form-info-box ol { - padding: 0; -} - -.form-info-box li { - list-style-position: inside; -} - -.show-info-btn { - width: max-content; - user-select: none; -} - -.show-info-btn:hover { - cursor: help; -} - -/* ruby { - display: inline-flex; - flex-direction: column-reverse; -} */ - -.pointer-text { - margin-bottom: 5px; - font-style: italic; -} - -.automated-result-text { - font-size: 0.9em; - text-align: right; - margin-bottom: 5px; -} - -.definition-list { - list-style-type: none; - padding: 0; -} - -html[data-page-type="popup"] .entry { - padding: 30px; -} diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index a1a409b703..3eefed538a 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -1,5 +1,4 @@ /* - * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2016-2022 Yomichan Authors * @@ -18,29 +17,29 @@ */ import * as wanakana from '../../lib/wanakana.js'; -import { AccessibilityController } from '../accessibility/accessibility-controller.js'; -import { AnkiConnect } from '../comm/anki-connect.js'; -import { ClipboardMonitor } from '../comm/clipboard-monitor.js'; -import { ClipboardReader } from '../comm/clipboard-reader.js'; -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 { OptionsUtil } from '../data/options-util.js'; -import { PermissionsUtil } from '../data/permissions-util.js'; -import { ArrayBufferUtil } from '../data/sandbox/array-buffer-util.js'; -import { Environment } from '../extension/environment.js'; -import { ObjectPropertyAccessor } from '../general/object-property-accessor.js'; -import { DictionaryDatabase } from '../language/dictionary-database.js'; -import { JapaneseUtil } from '../language/sandbox/japanese-util.js'; -import { Translator } from '../language/translator.js'; -import { AudioDownloader } from '../media/audio-downloader.js'; -import { MediaUtil } from '../media/media-util.js'; -import { yomitan } from '../yomitan.js'; -import { ClipboardReaderProxy, DictionaryDatabaseProxy, OffscreenProxy, TranslatorProxy } from './offscreen-proxy.js'; -import { ProfileConditionsUtil } from './profile-conditions-util.js'; -import { RequestBuilder } from './request-builder.js'; -import { ScriptManager } from './script-manager.js'; +import {AccessibilityController} from '../accessibility/accessibility-controller.js'; +import {AnkiConnect} from '../comm/anki-connect.js'; +import {ClipboardMonitor} from '../comm/clipboard-monitor.js'; +import {ClipboardReader} from '../comm/clipboard-reader.js'; +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 {OptionsUtil} from '../data/options-util.js'; +import {PermissionsUtil} from '../data/permissions-util.js'; +import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; +import {Environment} from '../extension/environment.js'; +import {ObjectPropertyAccessor} from '../general/object-property-accessor.js'; +import {DictionaryDatabase} from '../language/dictionary-database.js'; +import {JapaneseUtil} from '../language/sandbox/japanese-util.js'; +import {Translator} from '../language/translator.js'; +import {AudioDownloader} from '../media/audio-downloader.js'; +import {MediaUtil} from '../media/media-util.js'; +import {yomitan} from '../yomitan.js'; +import {ClipboardReaderProxy, DictionaryDatabaseProxy, OffscreenProxy, TranslatorProxy} from './offscreen-proxy.js'; +import {ProfileConditionsUtil} from './profile-conditions-util.js'; +import {RequestBuilder} from './request-builder.js'; +import {ScriptManager} from './script-manager.js'; /** * This class controls the core logic of the extension, including API calls @@ -129,7 +128,7 @@ export class Backend { /** @type {?Promise} */ this._preparePromise = null; /** @type {import('core').DeferredPromiseDetails} */ - const { promise, resolve, reject } = deferPromise(); + const {promise, resolve, reject} = deferPromise(); /** @type {Promise} */ this._prepareCompletePromise = promise; /** @type {() => void} */ @@ -149,63 +148,63 @@ export class Backend { this._permissionsUtil = new PermissionsUtil(); /** @type {import('backend').MessageHandlerMap} */ - this._messageHandlers = new Map(/** @type {import('backend').MessageHandlerMapInit} */([ - ['requestBackendReadySignal', { async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this) }], - ['optionsGet', { async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this) }], - ['optionsGetFull', { async: false, contentScript: true, handler: this._onApiOptionsGetFull.bind(this) }], - ['kanjiFind', { async: true, contentScript: true, handler: this._onApiKanjiFind.bind(this) }], - ['termsFind', { async: true, contentScript: true, handler: this._onApiTermsFind.bind(this) }], - ['parseText', { async: true, contentScript: true, handler: this._onApiParseText.bind(this) }], - ['getAnkiConnectVersion', { async: true, contentScript: true, handler: this._onApiGetAnkiConnectVersion.bind(this) }], - ['isAnkiConnected', { async: true, contentScript: true, handler: this._onApiIsAnkiConnected.bind(this) }], - ['addAnkiNote', { async: true, contentScript: true, handler: this._onApiAddAnkiNote.bind(this) }], - ['getAnkiNoteInfo', { async: true, contentScript: true, handler: this._onApiGetAnkiNoteInfo.bind(this) }], - ['injectAnkiNoteMedia', { async: true, contentScript: true, handler: this._onApiInjectAnkiNoteMedia.bind(this) }], - ['noteView', { async: true, contentScript: true, handler: this._onApiNoteView.bind(this) }], - ['suspendAnkiCardsForNote', { async: true, contentScript: true, handler: this._onApiSuspendAnkiCardsForNote.bind(this) }], - ['commandExec', { async: false, contentScript: true, handler: this._onApiCommandExec.bind(this) }], - ['getTermAudioInfoList', { async: true, contentScript: true, handler: this._onApiGetTermAudioInfoList.bind(this) }], - ['sendMessageToFrame', { async: false, contentScript: true, handler: this._onApiSendMessageToFrame.bind(this) }], - ['broadcastTab', { async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this) }], - ['frameInformationGet', { async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this) }], - ['injectStylesheet', { async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this) }], - ['getStylesheetContent', { async: true, contentScript: true, handler: this._onApiGetStylesheetContent.bind(this) }], - ['getEnvironmentInfo', { async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this) }], - ['clipboardGet', { async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this) }], - ['getDisplayTemplatesHtml', { async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this) }], - ['getZoom', { async: true, contentScript: true, handler: this._onApiGetZoom.bind(this) }], - ['getDefaultAnkiFieldTemplates', { async: false, contentScript: true, handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this) }], - ['getDictionaryInfo', { async: true, contentScript: true, handler: this._onApiGetDictionaryInfo.bind(this) }], - ['purgeDatabase', { async: true, contentScript: false, handler: this._onApiPurgeDatabase.bind(this) }], - ['getMedia', { async: true, contentScript: true, handler: this._onApiGetMedia.bind(this) }], - ['log', { async: false, contentScript: true, handler: this._onApiLog.bind(this) }], - ['logIndicatorClear', { async: false, contentScript: true, handler: this._onApiLogIndicatorClear.bind(this) }], - ['createActionPort', { async: false, contentScript: true, handler: this._onApiCreateActionPort.bind(this) }], - ['modifySettings', { async: true, contentScript: true, handler: this._onApiModifySettings.bind(this) }], - ['getSettings', { async: false, contentScript: true, handler: this._onApiGetSettings.bind(this) }], - ['setAllSettings', { async: true, contentScript: false, handler: this._onApiSetAllSettings.bind(this) }], - ['getOrCreateSearchPopup', { async: true, contentScript: true, handler: this._onApiGetOrCreateSearchPopup.bind(this) }], - ['isTabSearchPopup', { async: true, contentScript: true, handler: this._onApiIsTabSearchPopup.bind(this) }], - ['triggerDatabaseUpdated', { async: false, contentScript: true, handler: this._onApiTriggerDatabaseUpdated.bind(this) }], - ['testMecab', { async: true, contentScript: true, handler: this._onApiTestMecab.bind(this) }], - ['textHasJapaneseCharacters', { async: false, contentScript: true, handler: this._onApiTextHasJapaneseCharacters.bind(this) }], - ['getTermFrequencies', { async: true, contentScript: true, handler: this._onApiGetTermFrequencies.bind(this) }], - ['findAnkiNotes', { async: true, contentScript: true, handler: this._onApiFindAnkiNotes.bind(this) }], - ['loadExtensionScripts', { async: true, contentScript: true, handler: this._onApiLoadExtensionScripts.bind(this) }], - ['openCrossFramePort', { async: false, contentScript: true, handler: this._onApiOpenCrossFramePort.bind(this) }] + this._messageHandlers = new Map(/** @type {import('backend').MessageHandlerMapInit} */ ([ + ['requestBackendReadySignal', {async: false, contentScript: true, handler: this._onApiRequestBackendReadySignal.bind(this)}], + ['optionsGet', {async: false, contentScript: true, handler: this._onApiOptionsGet.bind(this)}], + ['optionsGetFull', {async: false, contentScript: true, handler: this._onApiOptionsGetFull.bind(this)}], + ['kanjiFind', {async: true, contentScript: true, handler: this._onApiKanjiFind.bind(this)}], + ['termsFind', {async: true, contentScript: true, handler: this._onApiTermsFind.bind(this)}], + ['parseText', {async: true, contentScript: true, handler: this._onApiParseText.bind(this)}], + ['getAnkiConnectVersion', {async: true, contentScript: true, handler: this._onApiGetAnkiConnectVersion.bind(this)}], + ['isAnkiConnected', {async: true, contentScript: true, handler: this._onApiIsAnkiConnected.bind(this)}], + ['addAnkiNote', {async: true, contentScript: true, handler: this._onApiAddAnkiNote.bind(this)}], + ['getAnkiNoteInfo', {async: true, contentScript: true, handler: this._onApiGetAnkiNoteInfo.bind(this)}], + ['injectAnkiNoteMedia', {async: true, contentScript: true, handler: this._onApiInjectAnkiNoteMedia.bind(this)}], + ['noteView', {async: true, contentScript: true, handler: this._onApiNoteView.bind(this)}], + ['suspendAnkiCardsForNote', {async: true, contentScript: true, handler: this._onApiSuspendAnkiCardsForNote.bind(this)}], + ['commandExec', {async: false, contentScript: true, handler: this._onApiCommandExec.bind(this)}], + ['getTermAudioInfoList', {async: true, contentScript: true, handler: this._onApiGetTermAudioInfoList.bind(this)}], + ['sendMessageToFrame', {async: false, contentScript: true, handler: this._onApiSendMessageToFrame.bind(this)}], + ['broadcastTab', {async: false, contentScript: true, handler: this._onApiBroadcastTab.bind(this)}], + ['frameInformationGet', {async: true, contentScript: true, handler: this._onApiFrameInformationGet.bind(this)}], + ['injectStylesheet', {async: true, contentScript: true, handler: this._onApiInjectStylesheet.bind(this)}], + ['getStylesheetContent', {async: true, contentScript: true, handler: this._onApiGetStylesheetContent.bind(this)}], + ['getEnvironmentInfo', {async: false, contentScript: true, handler: this._onApiGetEnvironmentInfo.bind(this)}], + ['clipboardGet', {async: true, contentScript: true, handler: this._onApiClipboardGet.bind(this)}], + ['getDisplayTemplatesHtml', {async: true, contentScript: true, handler: this._onApiGetDisplayTemplatesHtml.bind(this)}], + ['getZoom', {async: true, contentScript: true, handler: this._onApiGetZoom.bind(this)}], + ['getDefaultAnkiFieldTemplates', {async: false, contentScript: true, handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this)}], + ['getDictionaryInfo', {async: true, contentScript: true, handler: this._onApiGetDictionaryInfo.bind(this)}], + ['purgeDatabase', {async: true, contentScript: false, handler: this._onApiPurgeDatabase.bind(this)}], + ['getMedia', {async: true, contentScript: true, handler: this._onApiGetMedia.bind(this)}], + ['log', {async: false, contentScript: true, handler: this._onApiLog.bind(this)}], + ['logIndicatorClear', {async: false, contentScript: true, handler: this._onApiLogIndicatorClear.bind(this)}], + ['createActionPort', {async: false, contentScript: true, handler: this._onApiCreateActionPort.bind(this)}], + ['modifySettings', {async: true, contentScript: true, handler: this._onApiModifySettings.bind(this)}], + ['getSettings', {async: false, contentScript: true, handler: this._onApiGetSettings.bind(this)}], + ['setAllSettings', {async: true, contentScript: false, handler: this._onApiSetAllSettings.bind(this)}], + ['getOrCreateSearchPopup', {async: true, contentScript: true, handler: this._onApiGetOrCreateSearchPopup.bind(this)}], + ['isTabSearchPopup', {async: true, contentScript: true, handler: this._onApiIsTabSearchPopup.bind(this)}], + ['triggerDatabaseUpdated', {async: false, contentScript: true, handler: this._onApiTriggerDatabaseUpdated.bind(this)}], + ['testMecab', {async: true, contentScript: true, handler: this._onApiTestMecab.bind(this)}], + ['textHasJapaneseCharacters', {async: false, contentScript: true, handler: this._onApiTextHasJapaneseCharacters.bind(this)}], + ['getTermFrequencies', {async: true, contentScript: true, handler: this._onApiGetTermFrequencies.bind(this)}], + ['findAnkiNotes', {async: true, contentScript: true, handler: this._onApiFindAnkiNotes.bind(this)}], + ['loadExtensionScripts', {async: true, contentScript: true, handler: this._onApiLoadExtensionScripts.bind(this)}], + ['openCrossFramePort', {async: false, contentScript: true, handler: this._onApiOpenCrossFramePort.bind(this)}] ])); /** @type {import('backend').MessageHandlerWithProgressMap} */ - this._messageHandlersWithProgress = new Map(/** @type {import('backend').MessageHandlerWithProgressMapInit} */([ + this._messageHandlersWithProgress = new Map(/** @type {import('backend').MessageHandlerWithProgressMapInit} */ ([ // Empty ])); /** @type {Map void>} */ - this._commandHandlers = new Map(/** @type {[name: string, handler: (params?: import('core').SerializableObject) => void][]} */([ + this._commandHandlers = new Map(/** @type {[name: string, handler: (params?: import('core').SerializableObject) => void][]} */ ([ ['toggleTextScanning', this._onCommandToggleTextScanning.bind(this)], - ['openInfoPage', this._onCommandOpenInfoPage.bind(this)], - ['openSettingsPage', this._onCommandOpenSettingsPage.bind(this)], - ['openSearchPage', this._onCommandOpenSearchPage.bind(this)], - ['openPopupWindow', this._onCommandOpenPopupWindow.bind(this)] + ['openInfoPage', this._onCommandOpenInfoPage.bind(this)], + ['openSettingsPage', this._onCommandOpenSettingsPage.bind(this)], + ['openSearchPage', this._onCommandOpenSearchPage.bind(this)], + ['openPopupWindow', this._onCommandOpenPopupWindow.bind(this)] ])); } @@ -299,7 +298,7 @@ export class Backend { this._applyOptions('background'); - const options = this._getProfileOptions({ current: true }, false); + const options = this._getProfileOptions({current: true}, false); if (options.general.showGuide) { this._openWelcomeGuidePageOnce(); } @@ -307,7 +306,7 @@ export class Backend { this._clipboardMonitor.on('change', this._onClipboardTextChange.bind(this)); this._sendMessageAllTabsIgnoreResponse('Yomitan.backendReady', {}); - this._sendMessageIgnoreResponse({ action: 'Yomitan.backendReady', params: {} }); + this._sendMessageIgnoreResponse({action: 'Yomitan.backendReady', params: {}}); } catch (e) { log.error(e); throw e; @@ -324,14 +323,14 @@ export class Backend { /** * @param {{text: string}} params */ - async _onClipboardTextChange({ text }) { - const { clipboard: { maximumSearchLength } } = this._getProfileOptions({ current: true }, false); + async _onClipboardTextChange({text}) { + const {clipboard: {maximumSearchLength}} = this._getProfileOptions({current: true}, false); if (text.length > maximumSearchLength) { text = text.substring(0, maximumSearchLength); } try { - const { tab, created } = await this._getOrCreateSearchPopup(); - const { id } = tab; + const {tab, created} = await this._getOrCreateSearchPopup(); + const {id} = tab; if (typeof id !== 'number') { throw new Error('Tab does not have an id'); } @@ -345,7 +344,7 @@ export class Backend { /** * @param {{level: import('log').LogLevel}} params */ - _onLog({ level }) { + _onLog({level}) { const levelValue = this._getErrorLevelValue(level); if (levelValue <= this._getErrorLevelValue(this._logErrorLevel)) { return; } @@ -369,7 +368,7 @@ export class Backend { this._prepareCompletePromise.then( () => { handler(...args); }, - () => { } // NOP + () => {} // NOP ); }); } @@ -402,7 +401,7 @@ export class Backend { * @param {(response?: unknown) => void} callback * @returns {boolean} */ - _onMessage({ action, params }, sender, callback) { + _onMessage({action, params}, sender, callback) { const messageHandler = this._messageHandlers.get(action); if (typeof messageHandler === 'undefined') { return false; } @@ -410,7 +409,7 @@ export class Backend { try { this._validatePrivilegedMessageSender(sender); } catch (error) { - callback({ error: ExtensionError.serialize(error) }); + callback({error: ExtensionError.serialize(error)}); return false; } } @@ -421,8 +420,8 @@ export class Backend { /** * @param {chrome.tabs.ZoomChangeInfo} event */ - _onZoomChange({ tabId, oldZoomFactor, newZoomFactor }) { - this._sendMessageTabIgnoreResponse(tabId, { action: 'Yomitan.zoomChanged', params: { oldZoomFactor, newZoomFactor } }, {}); + _onZoomChange({tabId, oldZoomFactor, newZoomFactor}) { + this._sendMessageTabIgnoreResponse(tabId, {action: 'Yomitan.zoomChanged', params: {oldZoomFactor, newZoomFactor}}, {}); } /** @@ -435,7 +434,7 @@ export class Backend { /** * @param {chrome.runtime.InstalledDetails} event */ - _onInstalled({ reason }) { + _onInstalled({reason}) { if (reason !== 'install') { return; } this._requestPersistentStorage(); } @@ -445,12 +444,12 @@ export class Backend { /** @type {import('api').Handler} */ _onApiRequestBackendReadySignal(_params, sender) { // tab ID isn't set in background (e.g. browser_action) - const data = { action: 'Yomitan.backendReady', params: {} }; + const data = {action: 'Yomitan.backendReady', params: {}}; if (typeof sender.tab === 'undefined') { this._sendMessageIgnoreResponse(data); return false; } else { - const { id } = sender.tab; + const {id} = sender.tab; if (typeof id === 'number') { this._sendMessageTabIgnoreResponse(id, data, {}); } @@ -459,7 +458,7 @@ export class Backend { } /** @type {import('api').Handler} */ - _onApiOptionsGet({ optionsContext }) { + _onApiOptionsGet({optionsContext}) { return this._getProfileOptions(optionsContext, false); } @@ -469,9 +468,9 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiKanjiFind({ text, optionsContext }) { + async _onApiKanjiFind({text, optionsContext}) { const options = this._getProfileOptions(optionsContext, false); - const { general: { maxResults } } = options; + const {general: {maxResults}} = options; const findKanjiOptions = this._getTranslatorFindKanjiOptions(options); const dictionaryEntries = await this._translator.findKanji(text, findKanjiOptions); dictionaryEntries.splice(maxResults); @@ -479,17 +478,17 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiTermsFind({ text, details, optionsContext }) { + async _onApiTermsFind({text, details, optionsContext}) { const options = this._getProfileOptions(optionsContext, false); - const { general: { resultOutputMode: mode, maxResults } } = options; + const {general: {resultOutputMode: mode, maxResults}} = options; const findTermsOptions = this._getTranslatorFindTermsOptions(mode, details, options); - const { dictionaryEntries, originalTextLength } = await this._translator.findTerms(mode, text, findTermsOptions); + const {dictionaryEntries, originalTextLength} = await this._translator.findTerms(mode, text, findTermsOptions); dictionaryEntries.splice(maxResults); - return { dictionaryEntries, originalTextLength }; + return {dictionaryEntries, originalTextLength}; } /** @type {import('api').Handler} */ - async _onApiParseText({ text, optionsContext, scanLength, useInternalParser, useMecabParser }) { + async _onApiParseText({text, optionsContext, scanLength, useInternalParser, useMecabParser}) { const [internalResults, mecabResults] = await Promise.all([ (useInternalParser ? this._textParseScanning(text, scanLength, optionsContext) : null), (useMecabParser ? this._textParseMecab(text) : null) @@ -532,12 +531,12 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiAddAnkiNote({ note }) { + async _onApiAddAnkiNote({note}) { return await this._anki.addNote(note); } /** @type {import('api').Handler} */ - async _onApiGetAnkiNoteInfo({ notes, fetchAdditionalInfo }) { + async _onApiGetAnkiNoteInfo({notes, fetchAdditionalInfo}) { /** @type {import('anki').NoteInfoWrapper[]} */ const results = []; /** @type {{note: import('anki').Note, info: import('anki').NoteInfoWrapper}[]} */ @@ -549,15 +548,15 @@ export class Backend { let canAdd = canAddArray[i]; const valid = AnkiUtil.isNoteDataValid(note); if (!valid) { canAdd = false; } - const info = { canAdd, valid, noteIds: null }; + const info = {canAdd, valid, noteIds: null}; results.push(info); if (!canAdd && valid) { - cannotAdd.push({ note, info }); + cannotAdd.push({note, info}); } } if (cannotAdd.length > 0) { - const cannotAddNotes = cannotAdd.map(({ note }) => note); + const cannotAddNotes = cannotAdd.map(({note}) => note); const noteIdsArray = await this._anki.findNoteIds(cannotAddNotes); for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) { const noteIds = noteIdsArray[i]; @@ -574,7 +573,7 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiInjectAnkiNoteMedia({ timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails }) { + async _onApiInjectAnkiNoteMedia({timestamp, definitionDetails, audioDetails, screenshotDetails, clipboardDetails, dictionaryMediaDetails}) { return await this._injectAnkNoteMedia( this._anki, timestamp, @@ -587,7 +586,7 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiNoteView({ noteId, mode, allowFallback }) { + async _onApiNoteView({noteId, mode, allowFallback}) { if (mode === 'edit') { try { await this._anki.guiEditNote(noteId); @@ -606,7 +605,7 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiSuspendAnkiCardsForNote({ noteId }) { + async _onApiSuspendAnkiCardsForNote({noteId}) { const cardIds = await this._anki.findCardsForNote(noteId); const count = cardIds.length; if (count > 0) { @@ -617,39 +616,39 @@ export class Backend { } /** @type {import('api').Handler} */ - _onApiCommandExec({ command, params }) { + _onApiCommandExec({command, params}) { return this._runCommand(command, params); } /** @type {import('api').Handler} */ - async _onApiGetTermAudioInfoList({ source, term, reading }) { + async _onApiGetTermAudioInfoList({source, term, reading}) { return await this._audioDownloader.getTermAudioInfoList(source, term, reading); } /** @type {import('api').Handler} */ - _onApiSendMessageToFrame({ frameId: targetFrameId, action, params }, sender) { + _onApiSendMessageToFrame({frameId: targetFrameId, action, params}, sender) { if (!sender) { return false; } - const { tab } = sender; + const {tab} = sender; if (!tab) { return false; } - const { id } = tab; + const {id} = tab; if (typeof id !== 'number') { return false; } const frameId = sender.frameId; /** @type {import('extension').ChromeRuntimeMessageWithFrameId} */ - const message = { action, params, frameId }; - this._sendMessageTabIgnoreResponse(id, message, { frameId: targetFrameId }); + const message = {action, params, frameId}; + this._sendMessageTabIgnoreResponse(id, message, {frameId: targetFrameId}); return true; } /** @type {import('api').Handler} */ - _onApiBroadcastTab({ action, params }, sender) { + _onApiBroadcastTab({action, params}, sender) { if (!sender) { return false; } - const { tab } = sender; + const {tab} = sender; if (!tab) { return false; } - const { id } = tab; + const {id} = tab; if (typeof id !== 'number') { return false; } const frameId = sender.frameId; /** @type {import('extension').ChromeRuntimeMessageWithFrameId} */ - const message = { action, params, frameId }; + const message = {action, params, frameId}; this._sendMessageTabIgnoreResponse(id, message, {}); return true; } @@ -659,18 +658,18 @@ export class Backend { const tab = sender.tab; const tabId = tab ? tab.id : void 0; const frameId = sender.frameId; - return Promise.resolve({ tabId, frameId }); + return Promise.resolve({tabId, frameId}); } /** @type {import('api').Handler} */ - async _onApiInjectStylesheet({ type, value }, sender) { - const { frameId, tab } = sender; + 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); } /** @type {import('api').Handler} */ - async _onApiGetStylesheetContent({ url }) { + async _onApiGetStylesheetContent({url}) { if (!url.startsWith('/') || url.startsWith('//') || !url.endsWith('.css')) { throw new Error('Invalid URL'); } @@ -708,7 +707,7 @@ export class Backend { typeof chrome.tabs.getZoom === 'function' )) { // Not supported - resolve({ zoomFactor: 1.0 }); + resolve({zoomFactor: 1.0}); return; } chrome.tabs.getZoom(tabId, (zoomFactor) => { @@ -716,7 +715,7 @@ export class Backend { if (e) { reject(new Error(e.message)); } else { - resolve({ zoomFactor }); + resolve({zoomFactor}); } }); }); @@ -739,12 +738,12 @@ export class Backend { } /** @type {import('api').Handler} */ - async _onApiGetMedia({ targets }) { + async _onApiGetMedia({targets}) { return await this._getNormalizedDictionaryDatabaseMedia(targets); } /** @type {import('api').Handler} */ - _onApiLog({ error, level, context }) { + _onApiLog({error, level, context}) { log.log(ExtensionError.deserialize(error), level, context); } @@ -768,7 +767,7 @@ export class Backend { id }; - const port = chrome.tabs.connect(tabId, { name: JSON.stringify(details), frameId }); + const port = chrome.tabs.connect(tabId, {name: JSON.stringify(details), frameId}); try { this._createActionListenerPort(port, sender, this._messageHandlersWithProgress); } catch (e) { @@ -780,56 +779,56 @@ export class Backend { } /** @type {import('api').Handler} */ - _onApiModifySettings({ targets, source }) { + _onApiModifySettings({targets, source}) { return this._modifySettings(targets, source); } /** @type {import('api').Handler} */ - _onApiGetSettings({ targets }) { + _onApiGetSettings({targets}) { const results = []; for (const target of targets) { try { const result = this._getSetting(target); - results.push({ result: clone(result) }); + results.push({result: clone(result)}); } catch (e) { - results.push({ error: ExtensionError.serialize(e) }); + results.push({error: ExtensionError.serialize(e)}); } } return results; } /** @type {import('api').Handler} */ - async _onApiSetAllSettings({ value, source }) { + async _onApiSetAllSettings({value, source}) { this._optionsUtil.validate(value); this._options = clone(value); await this._saveOptions(source); } /** @type {import('api').Handler} */ - async _onApiGetOrCreateSearchPopup({ focus = false, text }) { - const { tab, created } = await this._getOrCreateSearchPopup(); + async _onApiGetOrCreateSearchPopup({focus=false, text}) { + const {tab, created} = await this._getOrCreateSearchPopup(); if (focus === true || (focus === 'ifCreated' && created)) { await this._focusTab(tab); } if (typeof text === 'string') { - const { id } = tab; + const {id} = tab; if (typeof id === 'number') { await this._updateSearchQuery(id, text, !created); } } - const { id } = tab; - return { tabId: typeof id === 'number' ? id : null, windowId: tab.windowId }; + const {id} = tab; + return {tabId: typeof id === 'number' ? id : null, windowId: tab.windowId}; } /** @type {import('api').Handler} */ - async _onApiIsTabSearchPopup({ tabId }) { + async _onApiIsTabSearchPopup({tabId}) { const baseUrl = chrome.runtime.getURL('/search.html'); const tab = typeof tabId === 'number' ? await this._checkTabUrl(tabId, (url) => url !== null && url.startsWith(baseUrl)) : null; return (tab !== null); } /** @type {import('api').Handler} */ - _onApiTriggerDatabaseUpdated({ type, cause }) { + _onApiTriggerDatabaseUpdated({type, cause}) { this._triggerDatabaseUpdated(type, cause); } @@ -841,7 +840,7 @@ export class Backend { let permissionsOkay = false; try { - permissionsOkay = await this._permissionsUtil.hasPermissions({ permissions: ['nativeMessaging'] }); + permissionsOkay = await this._permissionsUtil.hasPermissions({permissions: ['nativeMessaging']}); } catch (e) { // NOP } @@ -871,33 +870,33 @@ export class Backend { } /** @type {import('api').Handler} */ - _onApiTextHasJapaneseCharacters({ text }) { + _onApiTextHasJapaneseCharacters({text}) { return this._japaneseUtil.isStringPartiallyJapanese(text); } /** @type {import('api').Handler} */ - async _onApiGetTermFrequencies({ termReadingList, dictionaries }) { + async _onApiGetTermFrequencies({termReadingList, dictionaries}) { return await this._translator.getTermFrequencies(termReadingList, dictionaries); } /** @type {import('api').Handler} */ - async _onApiFindAnkiNotes({ query }) { + async _onApiFindAnkiNotes({query}) { return await this._anki.findNotes(query); } /** @type {import('api').Handler} */ - async _onApiLoadExtensionScripts({ files }, sender) { + async _onApiLoadExtensionScripts({files}, sender) { if (!sender || !sender.tab) { throw new Error('Invalid sender'); } const tabId = sender.tab.id; if (typeof tabId !== 'number') { throw new Error('Sender has invalid tab ID'); } - const { frameId } = sender; + const {frameId} = sender; for (const file of files) { await this._scriptManager.injectScript(file, tabId, frameId, false); } } /** @type {import('api').Handler} */ - _onApiOpenCrossFramePort({ targetTabId, targetFrameId }, sender) { + _onApiOpenCrossFramePort({targetTabId, targetFrameId}, sender) { const sourceTabId = (sender && sender.tab ? sender.tab.id : null); if (typeof sourceTabId !== 'number') { throw new Error('Port does not have an associated tab ID'); @@ -918,9 +917,9 @@ export class Backend { otherFrameId: sourceFrameId }; /** @type {?chrome.runtime.Port} */ - let sourcePort = chrome.tabs.connect(sourceTabId, { frameId: sourceFrameId, name: JSON.stringify(sourceDetails) }); + 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) }); + let targetPort = chrome.tabs.connect(targetTabId, {frameId: targetFrameId, name: JSON.stringify(targetDetails)}); const cleanup = () => { this._checkLastError(chrome.runtime.lastError); @@ -943,7 +942,7 @@ export class Backend { sourcePort.onDisconnect.addListener(cleanup); targetPort.onDisconnect.addListener(cleanup); - return { targetTabId, targetFrameId }; + return {targetTabId, targetFrameId}; } // Command handlers @@ -972,7 +971,7 @@ export class Backend { } /** @type {import('backend').FindTabsPredicate} */ - const predicate = ({ url: url2 }) => { + const predicate = ({url: url2}) => { if (url2 === null || !url2.startsWith(baseUrl)) { return false; } const parsedUrl = new URL(url2); const baseUrl2 = `${parsedUrl.origin}${parsedUrl.pathname}`; @@ -983,8 +982,8 @@ export class Backend { const openInTab = async () => { const tabInfo = /** @type {?import('backend').TabInfo} */ (await this._findTabs(1000, false, predicate, false)); if (tabInfo !== null) { - const { tab } = tabInfo; - const { id } = tab; + const {tab} = tabInfo; + const {id} = tab; if (typeof id === 'number') { await this._focusTab(tab); if (queryParams.query) { @@ -1034,14 +1033,14 @@ export class Backend { * @returns {Promise} */ async _onCommandToggleTextScanning() { - const options = this._getProfileOptions({ current: true }, false); + const options = this._getProfileOptions({current: true}, false); /** @type {import('settings-modifications').ScopedModificationSet} */ const modification = { action: 'set', path: 'general.enable', value: !options.general.enable, scope: 'profile', - optionsContext: { current: true } + optionsContext: {current: true} }; await this._modifySettings([modification], 'backend'); } @@ -1050,7 +1049,7 @@ export class Backend { * @returns {Promise} */ async _onCommandOpenPopupWindow() { - await this._onApiGetOrCreateSearchPopup({ focus: true }); + await this._onApiGetOrCreateSearchPopup({focus: true}); } // Utilities @@ -1066,9 +1065,9 @@ export class Backend { for (const target of targets) { try { const result = this._modifySetting(target); - results.push({ result: clone(result) }); + results.push({result: clone(result)}); } catch (e) { - results.push({ error: ExtensionError.serialize(e) }); + results.push({error: ExtensionError.serialize(e)}); } } await this._saveOptions(source); @@ -1101,7 +1100,7 @@ export class Backend { if (this._searchPopupTabId !== null) { const tab = await this._checkTabUrl(this._searchPopupTabId, urlPredicate); if (tab !== null) { - return { tab, created: false }; + return {tab, created: false}; } this._searchPopupTabId = null; } @@ -1110,10 +1109,10 @@ export class Backend { const existingTabInfo = await this._findSearchPopupTab(urlPredicate); if (existingTabInfo !== null) { const existingTab = existingTabInfo.tab; - const { id } = existingTab; + const {id} = existingTab; if (typeof id === 'number') { this._searchPopupTabId = id; - return { tab: existingTab, created: false }; + return {tab: existingTab, created: false}; } } @@ -1123,21 +1122,21 @@ export class Backend { } // Create a new window - const options = this._getProfileOptions({ current: true }, false); + const options = this._getProfileOptions({current: true}, false); const createData = this._getSearchPopupWindowCreateData(baseUrl, options); - const { popupWindow: { windowState } } = options; + const {popupWindow: {windowState}} = options; const popupWindow = await this._createWindow(createData); if (windowState !== 'normal' && typeof popupWindow.id === 'number') { - await this._updateWindow(popupWindow.id, { state: windowState }); + await this._updateWindow(popupWindow.id, {state: windowState}); } - const { tabs } = popupWindow; + const {tabs} = popupWindow; if (!Array.isArray(tabs) || tabs.length === 0) { throw new Error('Created window did not contain a tab'); } const tab = tabs[0]; - const { id } = tab; + const {id} = tab; if (typeof id !== 'number') { throw new Error('Tab does not have an id'); } @@ -1145,12 +1144,12 @@ export class Backend { await this._sendMessageTabPromise( id, - { action: 'SearchDisplayController.setMode', params: { mode: 'popup' } }, - { frameId: 0 } + {action: 'SearchDisplayController.setMode', params: {mode: 'popup'}}, + {frameId: 0} ); this._searchPopupTabId = id; - return { tab, created: true }; + return {tab, created: true}; } /** @@ -1159,14 +1158,14 @@ export class Backend { */ async _findSearchPopupTab(urlPredicate) { /** @type {import('backend').FindTabsPredicate} */ - const predicate = async ({ url, tab }) => { - const { id } = tab; + const predicate = async ({url, tab}) => { + const {id} = tab; if (typeof id === 'undefined' || !urlPredicate(url)) { return false; } try { const mode = await this._sendMessageTabPromise( id, - { action: 'SearchDisplayController.getMode', params: {} }, - { frameId: 0 } + {action: 'SearchDisplayController.getMode', params: {}}, + {frameId: 0} ); return mode === 'popup'; } catch (e) { @@ -1182,7 +1181,7 @@ export class Backend { * @returns {chrome.windows.CreateData} */ _getSearchPopupWindowCreateData(url, options) { - const { popupWindow: { width, height, left, top, useLeft, useTop, windowType } } = options; + const {popupWindow: {width, height, left, top, useLeft, useTop, windowType}} = options; return { url, width, @@ -1207,7 +1206,7 @@ export class Backend { if (error) { reject(new Error(error.message)); } else { - resolve(/** @type {chrome.windows.Window} */(result)); + resolve(/** @type {chrome.windows.Window} */ (result)); } } ); @@ -1245,8 +1244,8 @@ export class Backend { async _updateSearchQuery(tabId, text, animate) { await this._sendMessageTabPromise( tabId, - { action: 'SearchDisplayController.updateSearchQuery', params: { text, animate } }, - { frameId: 0 } + {action: 'SearchDisplayController.updateSearchQuery', params: {text, animate}}, + {frameId: 0} ); } @@ -1254,7 +1253,7 @@ export class Backend { * @param {string} source */ _applyOptions(source) { - const options = this._getProfileOptions({ current: true }, false); + const options = this._getProfileOptions({current: true}, false); this._updateBadge(); const enabled = options.general.enable; @@ -1276,7 +1275,7 @@ export class Backend { this._accessibilityController.update(this._getOptionsFull(false)); - this._sendMessageAllTabsIgnoreResponse('Yomitan.optionsUpdated', { source }); + this._sendMessageAllTabsIgnoreResponse('Yomitan.optionsUpdated', {source}); } /** @@ -1310,7 +1309,7 @@ export class Backend { const profiles = options.profiles; if (!optionsContext.current) { // Specific index - const { index } = optionsContext; + const {index} = optionsContext; if (typeof index === 'number') { if (index < 0 || index >= profiles.length) { throw this._createDataError(`Invalid profile index: ${index}`, optionsContext); @@ -1324,7 +1323,7 @@ export class Backend { } } // Default - const { profileCurrent } = options; + const {profileCurrent} = options; if (profileCurrent < 0 || profileCurrent >= profiles.length) { throw this._createDataError(`Invalid current profile index: ${profileCurrent}`, optionsContext); } @@ -1404,56 +1403,51 @@ export class Backend { * @param {import('settings').OptionsContext} optionsContext * @returns {Promise} */ - - /* https://github.com/seth-js/yomichan-de */ - // Don't use Japanese text segmentation since it breaks things async _textParseScanning(text, scanLength, optionsContext) { - // const jp = this._japaneseUtil; - // /** @type {import('translator').FindTermsMode} */ - // const mode = 'simple'; - // const options = this._getProfileOptions(optionsContext, false); - // const details = {matchType: /** @type {import('translation').FindTermsMatchType} */ ('exact'), deinflect: true}; - // const findTermsOptions = this._getTranslatorFindTermsOptions(mode, details, options); - // /** @type {import('api').ParseTextLine[]} */ - // const results = []; - // let previousUngroupedSegment = null; - // let i = 0; - // const ii = text.length; - // while (i < ii) { - // const {dictionaryEntries, originalTextLength} = await this._translator.findTerms( - // mode, - // text.substring(i, i + scanLength), - // findTermsOptions - // ); - // const codePoint = /** @type {number} */ (text.codePointAt(i)); - // const character = String.fromCodePoint(codePoint); - // if ( - // dictionaryEntries.length > 0 && - // originalTextLength > 0 && - // (originalTextLength !== character.length || jp.isCodePointJapanese(codePoint)) - // ) { - // previousUngroupedSegment = null; - // const {headwords: [{term, reading}]} = dictionaryEntries[0]; - // const source = text.substring(i, i + originalTextLength); - // const textSegments = []; - // for (const {text: text2, reading: reading2} of jp.distributeFuriganaInflected(term, reading, source)) { - // textSegments.push({text: text2, reading: reading2}); - // } - // results.push(textSegments); - // i += originalTextLength; - // } else { - // if (previousUngroupedSegment === null) { - // previousUngroupedSegment = {text: character, reading: ''}; - // results.push([previousUngroupedSegment]); - // } else { - // previousUngroupedSegment.text += character; - // } - // i += character.length; - // } - // } - // return results; - - return [[{ "text": text, "reading": "" }]]; + const jp = this._japaneseUtil; + /** @type {import('translator').FindTermsMode} */ + const mode = 'simple'; + const options = this._getProfileOptions(optionsContext, false); + const details = {matchType: /** @type {import('translation').FindTermsMatchType} */ ('exact'), deinflect: true}; + const findTermsOptions = this._getTranslatorFindTermsOptions(mode, details, options); + /** @type {import('api').ParseTextLine[]} */ + const results = []; + let previousUngroupedSegment = null; + let i = 0; + const ii = text.length; + while (i < ii) { + const {dictionaryEntries, originalTextLength} = await this._translator.findTerms( + mode, + text.substring(i, i + scanLength), + findTermsOptions + ); + const codePoint = /** @type {number} */ (text.codePointAt(i)); + const character = String.fromCodePoint(codePoint); + if ( + dictionaryEntries.length > 0 && + originalTextLength > 0 && + (originalTextLength !== character.length || jp.isCodePointJapanese(codePoint)) + ) { + previousUngroupedSegment = null; + const {headwords: [{term, reading}]} = dictionaryEntries[0]; + const source = text.substring(i, i + originalTextLength); + const textSegments = []; + for (const {text: text2, reading: reading2} of jp.distributeFuriganaInflected(term, reading, source)) { + textSegments.push({text: text2, reading: reading2}); + } + results.push(textSegments); + i += originalTextLength; + } else { + if (previousUngroupedSegment === null) { + previousUngroupedSegment = {text: character, reading: ''}; + results.push([previousUngroupedSegment]); + } else { + previousUngroupedSegment.text += character; + } + i += character.length; + } + } + return results; } /** @@ -1472,22 +1466,22 @@ export class Backend { /** @type {import('backend').MecabParseResults} */ const results = []; - for (const { name, lines } of parseTextResults) { + for (const {name, lines} of parseTextResults) { /** @type {import('api').ParseTextLine[]} */ const result = []; for (const line of lines) { - for (const { term, reading, source } of line) { + for (const {term, reading, source} of line) { const termParts = []; - for (const { text: text2, reading: reading2 } of jp.distributeFuriganaInflected( + for (const {text: text2, reading: reading2} of jp.distributeFuriganaInflected( term.length > 0 ? term : source, jp.convertKatakanaToHiragana(reading), source )) { - termParts.push({ text: text2, reading: reading2 }); + termParts.push({text: text2, reading: reading2}); } result.push(termParts); } - result.push([{ text: '\n', reading: '' }]); + result.push([{text: '\n', reading: ''}]); } results.push([name, result]); } @@ -1511,7 +1505,7 @@ export class Backend { const onProgress = (...data) => { try { if (done) { return; } - port.postMessage(/** @type {import('backend').InvokeWithProgressResponseProgressMessage} */({ type: 'progress', data })); + port.postMessage(/** @type {import('backend').InvokeWithProgressResponseProgressMessage} */ ({type: 'progress', data})); } catch (e) { // NOP } @@ -1524,7 +1518,7 @@ export class Backend { if (hasStarted) { return; } try { - const { action } = message; + const {action} = message; switch (action) { case 'fragment': messageString += message.data; @@ -1550,14 +1544,14 @@ export class Backend { */ const onMessageComplete = async (message) => { try { - const { action, params } = message; - port.postMessage(/** @type {import('backend').InvokeWithProgressResponseAcknowledgeMessage} */({ type: 'ack' })); + const {action, params} = message; + port.postMessage(/** @type {import('backend').InvokeWithProgressResponseAcknowledgeMessage} */ ({type: 'ack'})); const messageHandler = handlers.get(action); if (typeof messageHandler === 'undefined') { throw new Error('Invalid action'); } - const { handler, async, contentScript } = messageHandler; + const {handler, async, contentScript} = messageHandler; if (!contentScript) { this._validatePrivilegedMessageSender(sender); @@ -1565,7 +1559,7 @@ export class Backend { const promiseOrResult = handler(params, sender, onProgress); const result = async ? await promiseOrResult : promiseOrResult; - port.postMessage(/** @type {import('backend').InvokeWithProgressResponseCompleteMessage} */({ type: 'complete', data: result })); + port.postMessage(/** @type {import('backend').InvokeWithProgressResponseCompleteMessage} */ ({type: 'complete', data: result})); } catch (e) { cleanup(e); } @@ -1581,7 +1575,7 @@ export class Backend { const cleanup = (error) => { if (done) { return; } if (error !== null) { - port.postMessage(/** @type {import('backend').InvokeWithProgressResponseErrorMessage} */({ type: 'error', data: ExtensionError.serialize(error) })); + port.postMessage(/** @type {import('backend').InvokeWithProgressResponseErrorMessage} */ ({type: 'error', data: ExtensionError.serialize(error)})); } if (!hasStarted) { port.onMessage.removeListener(onMessage); @@ -1617,11 +1611,11 @@ export class Backend { const scope = target.scope; switch (scope) { case 'profile': - { - const { optionsContext } = target; - if (typeof optionsContext !== 'object' || optionsContext === null) { throw new Error('Invalid optionsContext'); } - return /** @type {import('settings').ProfileOptions} */ (this._getProfileOptions(optionsContext, true)); - } + { + const {optionsContext} = target; + if (typeof optionsContext !== 'object' || optionsContext === null) { throw new Error('Invalid optionsContext'); } + return /** @type {import('settings').ProfileOptions} */ (this._getProfileOptions(optionsContext, true)); + } case 'global': return /** @type {import('settings').Options} */ (this._getOptionsFull(true)); default: @@ -1637,7 +1631,7 @@ export class Backend { _getSetting(target) { const options = this._getModifySettingObject(target); const accessor = new ObjectPropertyAccessor(options); - const { path } = target; + const {path} = target; if (typeof path !== 'string') { throw new Error('Invalid path'); } return accessor.get(ObjectPropertyAccessor.getPathArray(path)); } @@ -1653,50 +1647,50 @@ export class Backend { const action = target.action; switch (action) { case 'set': - { - const { path, value } = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - const pathArray = ObjectPropertyAccessor.getPathArray(path); - accessor.set(pathArray, value); - return accessor.get(pathArray); - } + { + const {path, value} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + const pathArray = ObjectPropertyAccessor.getPathArray(path); + accessor.set(pathArray, value); + return accessor.get(pathArray); + } case 'delete': - { - const { path } = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - accessor.delete(ObjectPropertyAccessor.getPathArray(path)); - return true; - } + { + const {path} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + accessor.delete(ObjectPropertyAccessor.getPathArray(path)); + return true; + } case 'swap': - { - const { path1, path2 } = target; - if (typeof path1 !== 'string') { throw new Error('Invalid path1'); } - if (typeof path2 !== 'string') { throw new Error('Invalid path2'); } - accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2)); - return true; - } + { + const {path1, path2} = target; + if (typeof path1 !== 'string') { throw new Error('Invalid path1'); } + if (typeof path2 !== 'string') { throw new Error('Invalid path2'); } + accessor.swap(ObjectPropertyAccessor.getPathArray(path1), ObjectPropertyAccessor.getPathArray(path2)); + return true; + } case 'splice': - { - const { path, start, deleteCount, items } = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); } - if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); } - if (!Array.isArray(items)) { throw new Error('Invalid items'); } - const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); - if (!Array.isArray(array)) { throw new Error('Invalid target type'); } - return array.splice(start, deleteCount, ...items); - } + { + const {path, start, deleteCount, items} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + if (typeof start !== 'number' || Math.floor(start) !== start) { throw new Error('Invalid start'); } + if (typeof deleteCount !== 'number' || Math.floor(deleteCount) !== deleteCount) { throw new Error('Invalid deleteCount'); } + if (!Array.isArray(items)) { throw new Error('Invalid items'); } + const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); + if (!Array.isArray(array)) { throw new Error('Invalid target type'); } + return array.splice(start, deleteCount, ...items); + } case 'push': - { - const { path, items } = target; - if (typeof path !== 'string') { throw new Error('Invalid path'); } - if (!Array.isArray(items)) { throw new Error('Invalid items'); } - const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); - if (!Array.isArray(array)) { throw new Error('Invalid target type'); } - const start = array.length; - array.push(...items); - return start; - } + { + const {path, items} = target; + if (typeof path !== 'string') { throw new Error('Invalid path'); } + if (!Array.isArray(items)) { throw new Error('Invalid items'); } + const array = accessor.get(ObjectPropertyAccessor.getPathArray(path)); + if (!Array.isArray(array)) { throw new Error('Invalid target type'); } + const start = array.length; + array.push(...items); + return start; + } default: throw new Error(`Unknown action: ${action}`); } @@ -1707,11 +1701,11 @@ export class Backend { * @throws {Error} */ _validatePrivilegedMessageSender(sender) { - let { url } = sender; + let {url} = sender; if (typeof url === 'string' && yomitan.isExtensionUrl(url)) { return; } - const { tab } = sender; + const {tab} = sender; if (typeof tab === 'object' && tab !== null) { - ({ url } = tab); + ({url} = tab); if (typeof url === 'string' && yomitan.isExtensionUrl(url)) { return; } } throw new Error('Invalid message sender'); @@ -1723,7 +1717,7 @@ export class Backend { _getBrowserIconTitle() { return ( isObject(chrome.action) && - typeof chrome.action.getTitle === 'function' ? + typeof chrome.action.getTitle === 'function' ? new Promise((resolve) => chrome.action.getTitle({}, resolve)) : Promise.resolve('') ); @@ -1767,7 +1761,7 @@ export class Backend { status = 'Loading'; } } else { - const options = this._getProfileOptions({ current: true }, false); + const options = this._getProfileOptions({current: true}, false); if (!options.general.enable) { text = 'off'; color = '#555555'; @@ -1784,16 +1778,16 @@ export class Backend { } if (color !== null && typeof chrome.action.setBadgeBackgroundColor === 'function') { - chrome.action.setBadgeBackgroundColor({ color }); + chrome.action.setBadgeBackgroundColor({color}); } if (text !== null && typeof chrome.action.setBadgeText === 'function') { - chrome.action.setBadgeText({ text }); + chrome.action.setBadgeText({text}); } if (typeof chrome.action.setTitle === 'function') { if (status !== null) { title = `${title} - ${status}`; } - chrome.action.setTitle({ title }); + chrome.action.setTitle({title}); } } @@ -1802,7 +1796,7 @@ export class Backend { * @returns {boolean} */ _isAnyDictionaryEnabled(options) { - for (const { enabled } of options.dictionaries) { + for (const {enabled} of options.dictionaries) { if (enabled) { return true; } @@ -1818,8 +1812,8 @@ export class Backend { try { const response = await this._sendMessageTabPromise( tabId, - { action: 'Yomitan.getUrl', params: {} }, - { frameId: 0 } + {action: 'Yomitan.getUrl', params: {}}, + {frameId: 0} ); const url = typeof response === 'object' && response !== null ? /** @type {import('core').SerializableObject} */ (response).url : void 0; if (typeof url === 'string') { @@ -1864,13 +1858,13 @@ export class Backend { * @param {(tabInfo: import('backend').TabInfo) => boolean} add */ const checkTab = async (tab, add) => { - const { id } = tab; + const {id} = tab; const url = typeof id === 'number' ? await this._getTabUrl(id) : null; if (done) { return; } let okay = false; - const item = { tab, url }; + const item = {tab, url}; try { const okayOrPromise = predicate(item); okay = predicateIsAsync ? await okayOrPromise : /** @type {boolean} */ (okayOrPromise); @@ -1903,7 +1897,7 @@ export class Backend { ]); return results; } else { - const { promise, resolve } = /** @type {import('core').DeferredPromiseDetails} */ (deferPromise()); + const {promise, resolve} = /** @type {import('core').DeferredPromiseDetails} */ (deferPromise()); /** @type {?import('backend').TabInfo} */ let result = null; /** @@ -1931,12 +1925,12 @@ export class Backend { */ async _focusTab(tab) { await /** @type {Promise} */ (new Promise((resolve, reject) => { - const { id } = tab; + const {id} = tab; if (typeof id !== 'number') { reject(new Error('Cannot focus a tab without an id')); return; } - chrome.tabs.update(id, { active: true }, () => { + chrome.tabs.update(id, {active: true}, () => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); @@ -1964,7 +1958,7 @@ export class Backend { }); if (!tabWindow.focused) { await /** @type {Promise} */ (new Promise((resolve, reject) => { - chrome.windows.update(tab.windowId, { focused: true }, () => { + chrome.windows.update(tab.windowId, {focused: true}, () => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); @@ -1985,7 +1979,7 @@ export class Backend { * @param {?number} [timeout=null] * @returns {Promise} */ - _waitUntilTabFrameIsReady(tabId, frameId, timeout = null) { + _waitUntilTabFrameIsReady(tabId, frameId, timeout=null) { return new Promise((resolve, reject) => { /** @type {?import('core').Timeout} */ let timer = null; @@ -2017,14 +2011,14 @@ export class Backend { chrome.runtime.onMessage.addListener(onMessage); - this._sendMessageTabPromise(tabId, { action: 'Yomitan.isReady' }, { frameId }) + this._sendMessageTabPromise(tabId, {action: 'Yomitan.isReady'}, {frameId}) .then( (value) => { if (!value) { return; } cleanup(); resolve(); }, - () => { } // NOP + () => {} // NOP ); if (timeout !== null) { @@ -2100,9 +2094,9 @@ export class Backend { const callback = () => this._checkLastError(chrome.runtime.lastError); chrome.tabs.query({}, (tabs) => { for (const tab of tabs) { - const { id } = tab; + const {id} = tab; if (typeof id !== 'number') { continue; } - chrome.tabs.sendMessage(id, { action, params }, callback); + chrome.tabs.sendMessage(id, {action, params}, callback); } }); } @@ -2177,18 +2171,18 @@ export class Backend { */ async _getScreenshot(tabId, frameId, format, quality) { const tab = await this._getTabById(tabId); - const { windowId } = tab; + const {windowId} = tab; let token = null; try { if (typeof tabId === 'number' && typeof frameId === 'number') { const action = 'Frontend.setAllVisibleOverride'; - const params = { value: false, priority: 0, awaitFrame: true }; - token = await this._sendMessageTabPromise(tabId, { action, params }, { frameId }); + const params = {value: false, priority: 0, awaitFrame: true}; + token = await this._sendMessageTabPromise(tabId, {action, params}, {frameId}); } return await new Promise((resolve, reject) => { - chrome.tabs.captureVisibleTab(windowId, { format, quality }, (result) => { + chrome.tabs.captureVisibleTab(windowId, {format, quality}, (result) => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); @@ -2200,9 +2194,9 @@ export class Backend { } finally { if (token !== null) { const action = 'Frontend.clearAllVisibleOverride'; - const params = { token }; + const params = {token}; try { - await this._sendMessageTabPromise(tabId, { action, params }, { frameId }); + await this._sendMessageTabPromise(tabId, {action, params}, {frameId}); } catch (e) { // NOP } @@ -2263,7 +2257,7 @@ export class Backend { let dictionaryMedia; try { let errors2; - ({ results: dictionaryMedia, errors: errors2 } = await this._injectAnkiNoteDictionaryMedia(ankiConnect, timestamp, definitionDetails, dictionaryMediaDetails)); + ({results: dictionaryMedia, errors: errors2} = await this._injectAnkiNoteDictionaryMedia(ankiConnect, timestamp, definitionDetails, dictionaryMediaDetails)); for (const error of errors2) { errors.push(ExtensionError.serialize(error)); } @@ -2291,14 +2285,14 @@ export class Backend { */ async _injectAnkiNoteAudio(ankiConnect, timestamp, definitionDetails, details) { if (definitionDetails.type !== 'term') { return null; } - const { term, reading } = definitionDetails; + const {term, reading} = definitionDetails; if (term.length === 0 && reading.length === 0) { return null; } - const { sources, preferredAudioIndex, idleTimeout } = details; + const {sources, preferredAudioIndex, idleTimeout} = details; let data; let contentType; try { - ({ data, contentType } = await this._audioDownloader.downloadTermAudio( + ({data, contentType} = await this._audioDownloader.downloadTermAudio( sources, preferredAudioIndex, term, @@ -2327,10 +2321,10 @@ export class Backend { * @returns {Promise} */ async _injectAnkiNoteScreenshot(ankiConnect, timestamp, definitionDetails, details) { - const { tabId, frameId, format, quality } = details; + const {tabId, frameId, format, quality} = details; const dataUrl = await this._getScreenshot(tabId, frameId, format, quality); - const { mediaType, data } = this._getDataUrlInfo(dataUrl); + const {mediaType, data} = this._getDataUrlInfo(dataUrl); const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); if (extension === null) { throw new Error('Unknown media type for screenshot image'); @@ -2352,7 +2346,7 @@ export class Backend { return null; } - const { mediaType, data } = this._getDataUrlInfo(dataUrl); + const {mediaType, data} = this._getDataUrlInfo(dataUrl); const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); if (extension === null) { throw new Error('Unknown media type for clipboard image'); @@ -2373,9 +2367,9 @@ export class Backend { const targets = []; const detailsList = []; const detailsMap = new Map(); - for (const { dictionary, path } of dictionaryMediaDetails) { - const target = { dictionary, path }; - const details = { dictionary, path, media: null }; + for (const {dictionary, path} of dictionaryMediaDetails) { + const target = {dictionary, path}; + const details = {dictionary, path, media: null}; const key = JSON.stringify(target); targets.push(target); detailsList.push(details); @@ -2384,8 +2378,8 @@ export class Backend { const mediaList = await this._getNormalizedDictionaryDatabaseMedia(targets); for (const media of mediaList) { - const { dictionary, path } = media; - const key = JSON.stringify({ dictionary, path }); + const {dictionary, path} = media; + const key = JSON.stringify({dictionary, path}); const details = detailsMap.get(key); if (typeof details === 'undefined' || details.media !== null) { continue; } details.media = media; @@ -2395,10 +2389,10 @@ export class Backend { /** @type {import('api').InjectAnkiNoteDictionaryMediaResult[]} */ const results = []; for (let i = 0, ii = detailsList.length; i < ii; ++i) { - const { dictionary, path, media } = detailsList[i]; + const {dictionary, path, media} = detailsList[i]; let fileName = null; if (media !== null) { - const { content, mediaType } = media; + const {content, mediaType} = media; const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); fileName = this._generateAnkiNoteMediaFileName( `yomitan_dictionary_media_${i + 1}`, @@ -2413,10 +2407,10 @@ export class Backend { fileName = null; } } - results.push({ dictionary, path, fileName }); + results.push({dictionary, path, fileName}); } - return { results, errors }; + return {results, errors}; } /** @@ -2425,7 +2419,7 @@ export class Backend { */ _getAudioDownloadError(error) { if (error instanceof ExtensionError && typeof error.data === 'object' && error.data !== null) { - const { errors } = /** @type {import('core').SerializableObject} */ (error.data); + const {errors} = /** @type {import('core').SerializableObject} */ (error.data); if (Array.isArray(errors)) { for (const error2 of errors) { if (!(error2 instanceof Error)) { continue; } @@ -2433,9 +2427,9 @@ export class Backend { return this._createAudioDownloadError('Audio download was cancelled due to an idle timeout', 'audio-download-idle-timeout', errors); } if (!(error2 instanceof ExtensionError)) { continue; } - const { data } = error2; + const {data} = error2; if (!(typeof data === 'object' && data !== null)) { continue; } - const { details } = /** @type {import('core').SerializableObject} */ (data); + const {details} = /** @type {import('core').SerializableObject} */ (data); if (!(typeof details === 'object' && details !== null)) { continue; } const error3 = /** @type {import('core').SerializableObject} */ (details).error; if (typeof error3 !== 'string') { continue; } @@ -2494,13 +2488,13 @@ export class Backend { switch (definitionDetails.type) { case 'kanji': { - const { character } = definitionDetails; + const {character} = definitionDetails; if (character) { fileName += `_${character}`; } } break; default: { - const { reading, term } = definitionDetails; + const {reading, term} = definitionDetails; if (reading) { fileName += `_${reading}`; } if (term) { fileName += `_${term}`; } } @@ -2555,7 +2549,7 @@ export class Backend { let data = dataUrl.substring(match[0].length); if (typeof match[2] === 'undefined') { data = btoa(data); } - return { mediaType, data }; + return {mediaType, data}; } /** @@ -2564,7 +2558,7 @@ export class Backend { */ _triggerDatabaseUpdated(type, cause) { this._translator.clearDatabaseCaches(); - this._sendMessageAllTabsIgnoreResponse('Yomitan.databaseUpdated', { type, cause }); + this._sendMessageAllTabsIgnoreResponse('Yomitan.databaseUpdated', {type, cause}); } /** @@ -2585,13 +2579,13 @@ export class Backend { * @returns {import('translation').FindTermsOptions} An options object. */ _getTranslatorFindTermsOptions(mode, details, options) { - let { matchType, deinflect } = details; + let {matchType, deinflect} = details; if (typeof matchType !== 'string') { matchType = /** @type {import('translation').FindTermsMatchType} */ ('exact'); } if (typeof deinflect !== 'boolean') { deinflect = true; } const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options); const { - general: { mainDictionary, sortFrequencyDictionary, sortFrequencyDictionaryOrder }, - scanning: { alphanumeric }, + general: {mainDictionary, sortFrequencyDictionary, sortFrequencyDictionaryOrder}, + scanning: {alphanumeric}, translation: { convertHalfWidthCharacters, convertNumericCharacters, @@ -2639,7 +2633,7 @@ export class Backend { */ _getTranslatorFindKanjiOptions(options) { const enabledDictionaryMap = this._getTranslatorEnabledDictionaryMap(options); - return { enabledDictionaryMap }; + return {enabledDictionaryMap}; } /** @@ -2669,7 +2663,7 @@ export class Backend { for (const group of textReplacementsOptions.groups) { /** @type {import('translation').FindTermsTextReplacement[]} */ const textReplacementsEntries = []; - for (const { pattern, ignoreCase, replacement } of group) { + for (const {pattern, ignoreCase, replacement} of group) { let patternRegExp; try { patternRegExp = new RegExp(pattern, ignoreCase ? 'gi' : 'g'); @@ -2677,7 +2671,7 @@ export class Backend { // Invalid pattern continue; } - textReplacementsEntries.push({ pattern: patternRegExp, replacement }); + textReplacementsEntries.push({pattern: patternRegExp, replacement}); } if (textReplacementsEntries.length > 0) { textReplacements.push(textReplacementsEntries); @@ -2696,7 +2690,7 @@ export class Backend { chrome.storage.session.get(['openedWelcomePage']).then((result) => { if (!result.openedWelcomePage) { this._openWelcomeGuidePage(); - chrome.storage.session.set({ 'openedWelcomePage': true }); + chrome.storage.session.set({'openedWelcomePage': true}); } }); } @@ -2722,7 +2716,7 @@ export class Backend { const manifest = chrome.runtime.getManifest(); const optionsUI = manifest.options_ui; if (typeof optionsUI === 'undefined') { throw new Error('Failed to find options_ui'); } - const { page } = optionsUI; + const {page} = optionsUI; if (typeof page === 'undefined') { throw new Error('Failed to find options_ui.page'); } const url = chrome.runtime.getURL(page); switch (mode) { @@ -2750,7 +2744,7 @@ export class Backend { */ _createTab(url) { return new Promise((resolve, reject) => { - chrome.tabs.create({ url }, (tab) => { + chrome.tabs.create({url}, (tab) => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); @@ -2814,7 +2808,7 @@ export class Backend { // Only request this permission for Firefox versions >= 77. // https://bugzilla.mozilla.org/show_bug.cgi?id=1630413 - const { vendor, version } = await browser.runtime.getBrowserInfo(); + const {vendor, version} = await browser.runtime.getBrowserInfo(); if (vendor !== 'Mozilla') { return; } const match = /^\d+/.exec(version); @@ -2836,9 +2830,9 @@ export class Backend { async _getNormalizedDictionaryDatabaseMedia(targets) { const results = []; for (const item of await this._dictionaryDatabase.getMedia(targets)) { - const { content, dictionary, height, mediaType, path, width } = item; + const {content, dictionary, height, mediaType, path, width} = item; const content2 = ArrayBufferUtil.arrayBufferToBase64(content); - results.push({ content: content2, dictionary, height, mediaType, path, width }); + results.push({content: content2, dictionary, height, mediaType, path, width}); } return results; } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index b6cdcf9228..6e1450c3a1 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -1,5 +1,4 @@ /* - * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2017-2022 Yomichan Authors * @@ -36,42 +35,6 @@ import {ElementOverflowController} from './element-overflow-controller.js'; import {OptionToggleHotkeyHandler} from './option-toggle-hotkey-handler.js'; import {QueryParser} from './query-parser.js'; -/** - * - * @param {string} text - * @returns {string} - */ -function firstCharLower(text) { - /** - * @type {string[]} - */ - const chars = []; - - text.split('').forEach((char) => chars.push(char)); - - chars[0] = chars[0].toLowerCase(); - - return chars.join(''); -} - -/** - * - * @param {string} text - * @returns {string} - */ -function firstCharUpper(text) { - /** - * @type {string[]} - */ - const chars = []; - - text.split('').forEach((char) => chars.push(char)); - - chars[0] = chars[0].toUpperCase(); - - return chars.join(''); -} - /** * @augments EventDispatcher */ @@ -233,23 +196,23 @@ export class Display extends EventDispatcher { this._themeController = new ThemeController(document.documentElement); this._hotkeyHandler.registerActions([ - ['close', () => { this._onHotkeyClose(); }], - ['nextEntry', this._onHotkeyActionMoveRelative.bind(this, 1)], - ['previousEntry', this._onHotkeyActionMoveRelative.bind(this, -1)], - ['lastEntry', () => { this._focusEntry(this._dictionaryEntries.length - 1, 0, true); }], - ['firstEntry', () => { this._focusEntry(0, 0, true); }], - ['historyBackward', () => { this._sourceTermView(); }], - ['historyForward', () => { this._nextTermView(); }], + ['close', () => { this._onHotkeyClose(); }], + ['nextEntry', this._onHotkeyActionMoveRelative.bind(this, 1)], + ['previousEntry', this._onHotkeyActionMoveRelative.bind(this, -1)], + ['lastEntry', () => { this._focusEntry(this._dictionaryEntries.length - 1, 0, true); }], + ['firstEntry', () => { this._focusEntry(0, 0, true); }], + ['historyBackward', () => { this._sourceTermView(); }], + ['historyForward', () => { this._nextTermView(); }], ['copyHostSelection', () => this._copyHostSelection()], - ['nextEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(1, true); }], + ['nextEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(1, true); }], ['previousEntryDifferentDictionary', () => { this._focusEntryWithDifferentDictionary(-1, true); }] ]); this.registerDirectMessageHandlers([ - ['Display.setOptionsContext', {async: true, handler: this._onMessageSetOptionsContext.bind(this)}], - ['Display.setContent', {async: false, handler: this._onMessageSetContent.bind(this)}], - ['Display.setCustomCss', {async: false, handler: this._onMessageSetCustomCss.bind(this)}], - ['Display.setContentScale', {async: false, handler: this._onMessageSetContentScale.bind(this)}], - ['Display.configure', {async: true, handler: this._onMessageConfigure.bind(this)}], + ['Display.setOptionsContext', {async: true, handler: this._onMessageSetOptionsContext.bind(this)}], + ['Display.setContent', {async: false, handler: this._onMessageSetContent.bind(this)}], + ['Display.setCustomCss', {async: false, handler: this._onMessageSetCustomCss.bind(this)}], + ['Display.setContentScale', {async: false, handler: this._onMessageSetContentScale.bind(this)}], + ['Display.configure', {async: true, handler: this._onMessageConfigure.bind(this)}], ['Display.visibilityChanged', {async: false, handler: this._onMessageVisibilityChanged.bind(this)}] ]); this.registerWindowMessageHandlers([ @@ -591,14 +554,14 @@ export class Display extends EventDispatcher { /** @type {import('display').HistoryState} */ const newState = ( hasState ? - clone(state) : - { - focusEntry: 0, - optionsContext: void 0, - url: window.location.href, - sentence: {text: query, offset: 0}, - documentTitle: document.title - } + clone(state) : + { + focusEntry: 0, + optionsContext: void 0, + url: window.location.href, + sentence: {text: query, offset: 0}, + documentTitle: document.title + } ); if (!hasState || updateOptionsContext) { newState.optionsContext = clone(this._optionsContext); @@ -622,7 +585,7 @@ export class Display extends EventDispatcher { * @param {import('core').SerializableObject} [params] * @returns {Promise} */ - async invokeContentOrigin(action, params = {}) { + async invokeContentOrigin(action, params={}) { if (this._contentOriginTabId === this._tabId && this._contentOriginFrameId === this._frameId) { throw new Error('Content origin is same page'); } @@ -638,7 +601,7 @@ export class Display extends EventDispatcher { * @param {import('core').SerializableObject} [params] * @returns {Promise} */ - async invokeParentFrame(action, params = {}) { + async invokeParentFrame(action, params={}) { if (this._parentFrameId === null || this._parentFrameId === this._frameId) { throw new Error('Invalid parent frame'); } @@ -708,7 +671,7 @@ export class Display extends EventDispatcher { const messageHandler = this._windowMessageHandlers.get(action); if (typeof messageHandler === 'undefined') { return; } - const callback = () => { }; // NOP + const callback = () => {}; // NOP invokeMessageHandler(messageHandler, params, callback); } @@ -1200,49 +1163,7 @@ export class Display extends EventDispatcher { } } - /* https://github.com/seth-js/yomichan-de */ - /** - * @type {string[]} - */ - const matchedDefs = []; - /** - * @type {any[] | PromiseLike} - */ - const dictionaryEntries = []; - - const searches = [ - firstCharLower(source), - firstCharUpper(source), - source.toLowerCase(), - source - ]; - - // handle english apostrophe - if (/'|´/.test(source) && !/^'|^´/.test(source)) { - const noApostrophe = source.replace(/'.+/, '').replace(/´.+/, ''); - searches.push(...[firstCharLower(noApostrophe), firstCharUpper(noApostrophe), noApostrophe.toLowerCase()]); - } - - for (const search of searches) { - const result = await yomitan.api.termsFind( - search, - findDetails, - optionsContext - ); - - if (result.dictionaryEntries.length > 0) { - result.dictionaryEntries.forEach((entry) => { - const {definitions} = entry; - - // avoid duplicate results - if (!matchedDefs.includes(JSON.stringify(definitions))) { - matchedDefs.push(JSON.stringify(definitions)); - dictionaryEntries.push(entry); - } - }); - } - } - + const {dictionaryEntries} = await yomitan.api.termsFind(source, findDetails, optionsContext); return dictionaryEntries; } } @@ -1342,8 +1263,8 @@ export class Display extends EventDispatcher { const dictionaryEntry = dictionaryEntries[i]; const entry = ( dictionaryEntry.type === 'term' ? - this._displayGenerator.createTermEntry(dictionaryEntry) : - this._displayGenerator.createKanjiEntry(dictionaryEntry) + this._displayGenerator.createTermEntry(dictionaryEntry) : + this._displayGenerator.createKanjiEntry(dictionaryEntry) ); entry.dataset.index = `${i}`; this._dictionaryEntryNodes.push(entry); @@ -1365,102 +1286,6 @@ export class Display extends EventDispatcher { this._windowScroll.to(x, y); } - /* https://github.com/seth-js/yomichan-de */ - for (const entryElem of Array.from(document.querySelectorAll('#dictionary-entries .entry'))) { - const formBoxes = new Map(); - - for (const inflectElem of entryElem.querySelectorAll('.inflection')) { - if (inflectElem.textContent === null) { return; } - - const [targetPOS] = inflectElem.textContent.split(' '); - - const inflectTextArr = inflectElem.textContent.split(' '); - inflectTextArr.shift(); - let inflectText = inflectTextArr.join(' '); - - if (typeof formBoxes.get(targetPOS) === 'undefined') { formBoxes.set(targetPOS, {}); } - const targetPOSBox = formBoxes.get(targetPOS); - - if (!targetPOSBox.inflections) { targetPOSBox.inflections = []; } - targetPOSBox.isAutomated = false; - - if (/-automated-/.test(inflectText)) { - inflectText = inflectText.replace(/^-.+?- /, ''); - targetPOSBox.isAutomated = true; - } - - const pointerText = inflectText.replace(/\}.+/, '').replace(/\{/, ''); - inflectText = inflectText.replace(/\{.+?\} /, ''); - - targetPOSBox.pointerText = pointerText; - - targetPOSBox.inflections.push(inflectText); - } - - for (const defElem of Array.from(entryElem.querySelectorAll('.definition-item'))) { - for (const tagElem of Array.from(defElem.querySelectorAll('.tag[data-category="partOfSpeech"]'))) { - const pos = tagElem.textContent; - if (pos === null) { return; } - if (formBoxes.get(pos)) { - const formInfoBox = document.createElement('div'); - - formInfoBox.classList.add('form-info-box'); - - if (formBoxes.get(pos).isAutomated) { - const automatedNotice = document.createElement('div'); - automatedNotice.classList.add('automated-result-text'); - automatedNotice.textContent = '(automated results)'; - formInfoBox.appendChild(automatedNotice); - } - - const pointerElem = document.createElement('div'); - pointerElem.classList.add('pointer-text'); - pointerElem.textContent = formBoxes.get(pos).pointerText; - formInfoBox.appendChild(pointerElem); - - const reasonList = document.createElement('ol'); - - for (const reason of formBoxes.get(pos).inflections) { - const item = document.createElement('li'); - item.textContent = reason; - reasonList.appendChild(item); - } - - formInfoBox.appendChild(reasonList); - - const definitionTagList = defElem.querySelector('.definition-tag-list'); - if (definitionTagList === null) { return; } - definitionTagList.append(formInfoBox); - } - } - - if (defElem.querySelector('.form-info-box')) { - defElem.addEventListener('mouseleave', (e) => { - for (const boxElem of /** @type {NodeListOf} */ (defElem.querySelectorAll('.form-info-box'))) { - boxElem.style.display = 'none'; - } - }); - - const showBoxIcon = document.createElement('span'); - showBoxIcon.textContent = 'ⓘ'; - showBoxIcon.classList.add('show-info-btn'); - - showBoxIcon.addEventListener('mouseenter', (e) => { - for (const boxElem of /** @type {NodeListOf} */ (defElem.querySelectorAll('.form-info-box'))) { - boxElem.style.display = 'block'; - } - }); - - const formIntoBox = defElem.querySelector('.form-info-box'); - if (formIntoBox === null) { return; } - formIntoBox.before(showBoxIcon); - } - } - } - - // ============================== - - this._triggerContentUpdateComplete(); } @@ -1814,8 +1639,8 @@ export class Display extends EventDispatcher { _isQueryParserVisible() { return ( this._queryParserVisibleOverride !== null ? - this._queryParserVisibleOverride : - this._queryParserVisible + this._queryParserVisibleOverride : + this._queryParserVisible ); } @@ -1853,8 +1678,8 @@ export class Display extends EventDispatcher { typeof this._tabId === 'number' && ( (isSearchPage) ? - (options.scanning.enableOnSearchPage) : - (this._depth < options.scanning.popupNestingMaxDepth) + (options.scanning.enableOnSearchPage) : + (this._depth < options.scanning.popupNestingMaxDepth) ) ); diff --git a/ext/js/language/dictionary-database.js b/ext/js/language/dictionary-database.js index 75965d048e..c47e1e902f 100644 --- a/ext/js/language/dictionary-database.js +++ b/ext/js/language/dictionary-database.js @@ -16,8 +16,8 @@ * along with this program. If not, see . */ -import { log, stringReverse } from '../core.js'; -import { Database } from '../data/database.js'; +import {log, stringReverse} from '../core.js'; +import {Database} from '../data/database.js'; export class DictionaryDatabase { constructor() { @@ -63,19 +63,19 @@ export class DictionaryDatabase { version: 20, stores: { terms: { - primaryKey: { keyPath: 'id', autoIncrement: true }, + primaryKey: {keyPath: 'id', autoIncrement: true}, indices: ['dictionary', 'expression', 'reading'] }, kanji: { - primaryKey: { autoIncrement: true }, + primaryKey: {autoIncrement: true}, indices: ['dictionary', 'character'] }, tagMeta: { - primaryKey: { autoIncrement: true }, + primaryKey: {autoIncrement: true}, indices: ['dictionary'] }, dictionaries: { - primaryKey: { autoIncrement: true }, + primaryKey: {autoIncrement: true}, indices: ['title', 'version'] } } @@ -84,15 +84,15 @@ export class DictionaryDatabase { version: 30, stores: { termMeta: { - primaryKey: { autoIncrement: true }, + primaryKey: {autoIncrement: true}, indices: ['dictionary', 'expression'] }, kanjiMeta: { - primaryKey: { autoIncrement: true }, + primaryKey: {autoIncrement: true}, indices: ['dictionary', 'character'] }, tagMeta: { - primaryKey: { autoIncrement: true }, + primaryKey: {autoIncrement: true}, indices: ['dictionary', 'name'] } } @@ -101,7 +101,7 @@ export class DictionaryDatabase { version: 40, stores: { terms: { - primaryKey: { keyPath: 'id', autoIncrement: true }, + primaryKey: {keyPath: 'id', autoIncrement: true}, indices: ['dictionary', 'expression', 'reading', 'sequence'] } } @@ -110,7 +110,7 @@ export class DictionaryDatabase { version: 50, stores: { terms: { - primaryKey: { keyPath: 'id', autoIncrement: true }, + primaryKey: {keyPath: 'id', autoIncrement: true}, indices: ['dictionary', 'expression', 'reading', 'sequence', 'expressionReverse', 'readingReverse'] } } @@ -119,7 +119,7 @@ export class DictionaryDatabase { version: 60, stores: { media: { - primaryKey: { keyPath: 'id', autoIncrement: true }, + primaryKey: {keyPath: 'id', autoIncrement: true}, indices: ['dictionary', 'path'] } } @@ -235,7 +235,7 @@ export class DictionaryDatabase { /** @type {import('dictionary-database').FindPredicate} */ const predicate = (row) => { if (!dictionaries.has(row.dictionary)) { return false; } - const { id } = row; + const {id} = row; if (visited.has(id)) { return false; } visited.add(id); return true; @@ -373,19 +373,19 @@ export class DictionaryDatabase { const databaseTargets = targets.map(([objectStoreName, indexName]) => { const objectStore = transaction.objectStore(objectStoreName); const index = objectStore.index(indexName); - return { objectStore, index }; + return {objectStore, index}; }); /** @type {import('database').CountTarget[]} */ const countTargets = []; if (getTotal) { - for (const { objectStore } of databaseTargets) { + for (const {objectStore} of databaseTargets) { countTargets.push([objectStore, void 0]); } } for (const dictionaryName of dictionaryNames) { const query = IDBKeyRange.only(dictionaryName); - for (const { index } of databaseTargets) { + for (const {index} of databaseTargets) { countTargets.push([index, query]); } } @@ -407,7 +407,7 @@ export class DictionaryDatabase { counts.push(countGroup); } const total = getTotal ? /** @type {import('dictionary-database').DictionaryCountGroup} */ (counts.shift()) : null; - resolve({ total, counts }); + resolve({total, counts}); }; this._db.bulkCount(countTargets, onCountComplete, reject); @@ -488,7 +488,7 @@ export class DictionaryDatabase { const query = createQuery(item); for (let j = 0; j < indexCount; ++j) { /** @type {import('dictionary-database').FindMultiBulkData} */ - const data = { item, itemIndex: i, indexIndex: j }; + const data = {item, itemIndex: i, indexIndex: j}; this._db.getAll(indexList[j], query, onGetAll, reject, data); } } @@ -578,7 +578,7 @@ export class DictionaryDatabase { * @returns {import('dictionary-database').TermEntry} */ _createTerm(matchSource, matchType, row, index) { - const { sequence } = row; + const {sequence} = row; return { index, matchType, @@ -592,8 +592,7 @@ export class DictionaryDatabase { score: row.score, dictionary: row.dictionary, id: row.id, - sequence: typeof sequence === 'number' ? sequence : -1, - skip: false, + sequence: typeof sequence === 'number' ? sequence : -1 }; } @@ -602,8 +601,8 @@ export class DictionaryDatabase { * @param {import('dictionary-database').FindMultiBulkData} data * @returns {import('dictionary-database').KanjiEntry} */ - _createKanji(row, { itemIndex: index }) { - const { stats } = row; + _createKanji(row, {itemIndex: index}) { + const {stats} = row; return { index, character: row.character, @@ -622,12 +621,12 @@ export class DictionaryDatabase { * @returns {import('dictionary-database').TermMeta} * @throws {Error} */ - _createTermMeta({ expression: term, mode, data, dictionary }, { itemIndex: index }) { + _createTermMeta({expression: term, mode, data, dictionary}, {itemIndex: index}) { switch (mode) { case 'freq': - return { index, term, mode, data, dictionary }; + return {index, term, mode, data, dictionary}; case 'pitch': - return { index, term, mode, data, dictionary }; + return {index, term, mode, data, dictionary}; default: throw new Error(`Unknown mode: ${mode}`); } @@ -638,8 +637,8 @@ export class DictionaryDatabase { * @param {import('dictionary-database').FindMultiBulkData} data * @returns {import('dictionary-database').KanjiMeta} */ - _createKanjiMeta({ character, mode, data, dictionary }, { itemIndex: index }) { - return { index, character, mode, data, dictionary }; + _createKanjiMeta({character, mode, data, dictionary}, {itemIndex: index}) { + return {index, character, mode, data, dictionary}; } /** @@ -647,9 +646,9 @@ export class DictionaryDatabase { * @param {import('dictionary-database').FindMultiBulkData} data * @returns {import('dictionary-database').Media} */ - _createMedia(row, { itemIndex: index }) { - const { dictionary, path, mediaType, width, height, content } = row; - return { index, dictionary, path, mediaType, width, height, content }; + _createMedia(row, {itemIndex: index}) { + const {dictionary, path, mediaType, width, height, content} = row; + return {index, dictionary, path, mediaType, width, height, content}; } /** diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js index 24d5608e42..d1b033e6f2 100644 --- a/ext/js/language/text-scanner.js +++ b/ext/js/language/text-scanner.js @@ -1,5 +1,4 @@ /* - * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2019-2022 Yomichan Authors * @@ -17,10 +16,10 @@ * along with this program. If not, see . */ -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'; +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'; /** * @augments EventDispatcher @@ -32,12 +31,12 @@ export class TextScanner extends EventDispatcher { constructor({ node, getSearchContext, - ignoreElements = null, - ignorePoint = null, - searchTerms = false, - searchKanji = false, - searchOnClick = false, - searchOnClickOnly = false + ignoreElements=null, + ignorePoint=null, + searchTerms=false, + searchKanji=false, + searchOnClick=false, + searchOnClickOnly=false }) { super(); /** @type {HTMLElement|Window} */ @@ -271,7 +270,7 @@ export class TextScanner extends EventDispatcher { this._matchTypePrefix = matchTypePrefix; } if (typeof sentenceParsingOptions === 'object' && sentenceParsingOptions !== null) { - const { scanExtent, terminationCharacterMode, terminationCharacters } = sentenceParsingOptions; + const {scanExtent, terminationCharacterMode, terminationCharacters} = sentenceParsingOptions; if (typeof scanExtent === 'number') { this._sentenceScanExtent = scanExtent; } @@ -288,7 +287,7 @@ export class TextScanner extends EventDispatcher { Array.isArray(terminationCharacters) && (terminationCharacterMode === 'custom' || terminationCharacterMode === 'custom-no-newlines') ) { - for (const { enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd } of terminationCharacters) { + for (const {enabled, character1, character2, includeCharacterAtStart, includeCharacterAtEnd} of terminationCharacters) { if (!enabled) { continue; } if (character2 === null) { sentenceTerminatorMap.set(character1, [includeCharacterAtStart, includeCharacterAtEnd]); @@ -404,7 +403,7 @@ export class TextScanner extends EventDispatcher { */ _createOptionsContextForInput(baseOptionsContext, inputInfo) { const optionsContext = clone(baseOptionsContext); - const { modifiers, modifierKeys } = inputInfo; + const {modifiers, modifierKeys} = inputInfo; optionsContext.modifiers = [...modifiers]; optionsContext.modifierKeys = [...modifierKeys]; return optionsContext; @@ -436,8 +435,8 @@ export class TextScanner extends EventDispatcher { const inputInfoDetail = inputInfo.detail; const selectionRestoreInfo = ( (typeof inputInfoDetail === 'object' && inputInfoDetail !== null && inputInfoDetail.restoreSelection) ? - (this._inputInfoCurrent === null ? this._createSelectionRestoreInfo() : null) : - null + (this._inputInfoCurrent === null ? this._createSelectionRestoreInfo() : null) : + null ); if (this._textSourceCurrent !== null && this._textSourceCurrent.hasSameStart(textSource)) { @@ -446,7 +445,7 @@ export class TextScanner extends EventDispatcher { const getSearchContextPromise = this._getSearchContext(); const getSearchContextResult = getSearchContextPromise instanceof Promise ? await getSearchContextPromise : getSearchContextPromise; - const { detail: detail2 } = getSearchContextResult; + const {detail: detail2} = getSearchContextResult; if (typeof detail2 !== 'undefined') { detail = detail2; } optionsContext = this._createOptionsContextForInput(getSearchContextResult.optionsContext, inputInfo); @@ -455,11 +454,11 @@ export class TextScanner extends EventDispatcher { let valid = false; const result = await this._findDictionaryEntries(textSource, searchTerms, searchKanji, optionsContext); if (result !== null) { - ({ dictionaryEntries, sentence, type } = result); + ({dictionaryEntries, sentence, type} = result); valid = true; } else if (textSource !== null && textSource instanceof TextSourceElement && await this._hasJapanese(textSource.fullContent)) { dictionaryEntries = []; - sentence = { text: '', offset: 0 }; + sentence = {text: '', offset: 0}; type = 'terms'; valid = true; } @@ -534,7 +533,7 @@ export class TextScanner extends EventDispatcher { * @param {MouseEvent} e */ _onMouseOver(e) { - if (this._ignoreElements !== null && this._ignoreElements().includes(/** @type {Element} */(e.target))) { + if (this._ignoreElements !== null && this._ignoreElements().includes(/** @type {Element} */ (e.target))) { this._scanTimerClear(); } } @@ -647,7 +646,7 @@ export class TextScanner extends EventDispatcher { return; } - const { clientX, clientY, identifier } = e.changedTouches[0]; + const {clientX, clientY, identifier} = e.changedTouches[0]; this._onPrimaryTouchStart(e, clientX, clientY, identifier); } @@ -687,7 +686,7 @@ export class TextScanner extends EventDispatcher { const primaryTouch = this._getTouch(e.changedTouches, this._primaryTouchIdentifier); if (primaryTouch === null) { return; } - const { clientX, clientY } = primaryTouch; + const {clientX, clientY} = primaryTouch; this._onPrimaryTouchEnd(e, clientX, clientY, true); } @@ -743,7 +742,7 @@ export class TextScanner extends EventDispatcher { const inputInfo = this._getMatchingInputGroupFromEvent('touch', 'touchMove', e); if (inputInfo === null) { return; } - const { input } = inputInfo; + const {input} = inputInfo; if (input !== null && input.scanOnTouchMove) { this._searchAt(primaryTouch.clientX, primaryTouch.clientY, inputInfo); } @@ -756,7 +755,7 @@ export class TextScanner extends EventDispatcher { * @returns {boolean|void} */ _onPointerOver(e) { - const { pointerType, pointerId, isPrimary } = e; + const {pointerType, pointerId, isPrimary} = e; if (pointerType === 'pen') { this._pointerIdTypeMap.set(pointerId, pointerType); } @@ -889,7 +888,7 @@ export class TextScanner extends EventDispatcher { * @returns {boolean|void} */ _onTouchPointerDown(e) { - const { clientX, clientY, pointerId } = e; + const {clientX, clientY, pointerId} = e; this._onPrimaryTouchStart(e, clientX, clientY, pointerId); } @@ -913,7 +912,7 @@ export class TextScanner extends EventDispatcher { * @returns {boolean|void} */ _onTouchPointerUp(e) { - const { clientX, clientY } = e; + const {clientX, clientY} = e; return this._onPrimaryTouchEnd(e, clientX, clientY, true); } @@ -1072,7 +1071,7 @@ export class TextScanner extends EventDispatcher { [this._node, 'pointerup', this._onPointerUp.bind(this), capture], [this._node, 'pointercancel', this._onPointerCancel.bind(this), capture], [this._node, 'pointerout', this._onPointerOut.bind(this), capture], - [this._node, 'touchmove', this._onTouchMovePreventScroll.bind(this), { passive: false, capture }], + [this._node, 'touchmove', this._onTouchMovePreventScroll.bind(this), {passive: false, capture}], [this._node, 'mousedown', this._onMouseDown.bind(this), capture], [this._node, 'click', this._onClick.bind(this), capture], [this._node, 'auxclick', this._onAuxClick.bind(this), capture] @@ -1103,7 +1102,7 @@ export class TextScanner extends EventDispatcher { [this._node, 'touchstart', this._onTouchStart.bind(this), capture], [this._node, 'touchend', this._onTouchEnd.bind(this), capture], [this._node, 'touchcancel', this._onTouchCancel.bind(this), capture], - [this._node, 'touchmove', this._onTouchMove.bind(this), { passive: false, capture }], + [this._node, 'touchmove', this._onTouchMove.bind(this), {passive: false, capture}], [this._node, 'contextmenu', this._onContextMenu.bind(this), capture] ]; } @@ -1123,7 +1122,7 @@ export class TextScanner extends EventDispatcher { * @returns {import('event-listener-collection').AddEventListenerArgs[]} */ _getMouseClickOnlyEventListeners2(capture) { - const { documentElement } = document; + const {documentElement} = document; /** @type {import('event-listener-collection').AddEventListenerArgs[]} */ const entries = [ [document, 'selectionchange', this._onSelectionChange.bind(this)] @@ -1199,72 +1198,7 @@ export class TextScanner extends EventDispatcher { /** @type {import('api').FindTermsDetails} */ const details = {}; if (this._matchTypePrefix) { details.matchType = 'prefix'; } - - // const {dictionaryEntries, originalTextLength} = await yomitan.api.termsFind(searchText, details, optionsContext); - // if (dictionaryEntries.length === 0) { return null; } - - /* https://github.com/seth-js/yomichan-de */ - /** @type {string[]} */ - const matchedDefs = []; - /** @type {import('dictionary').TermDictionaryEntry[]} */ - const dictionaryEntries = []; - let originalTextLength = 0; - - /** - * @param {string} text - * @returns {string} - */ - function firstCharLower(text) { - /** @type {string[]} */ - let chars = []; - - text.split('').forEach((char) => chars.push(char)); - - chars[0] = chars[0].toLowerCase(); - - return chars.join(''); - } - - /** - * @param {string} text - * @returns {string} - */ - function firstCharUpper(text) { - /** @type {string[]} */ - let chars = []; - - text.split('').forEach((char) => chars.push(char)); - - chars[0] = chars[0].toUpperCase(); - - return chars.join(''); - } - - const searches = [firstCharLower(searchText), firstCharUpper(searchText), searchText.toLowerCase(), searchText]; - - // handle english apostrophe - if (/'|´/.test(searchText) && !/^'|^´/.test(searchText)) { - const noApostrophe = searchText.replace(/'.+/, '').replace(/´.+/, ''); - searches.push(...[firstCharLower(noApostrophe), firstCharUpper(noApostrophe), noApostrophe.toLowerCase()]); - } - - for (const search of searches) { - const result = await yomitan.api.termsFind(search, details, optionsContext); - - if (result.dictionaryEntries.length > 0) { - result.dictionaryEntries.forEach((entry) => { - const { definitions } = entry; - - // avoid duplicate results - if (!matchedDefs.includes(JSON.stringify(definitions))) { - matchedDefs.push(JSON.stringify(definitions)); - dictionaryEntries.push(entry); - originalTextLength = result.originalTextLength; - } - }); - } - } - + const {dictionaryEntries, originalTextLength} = await yomitan.api.termsFind(searchText, details, optionsContext); if (dictionaryEntries.length === 0) { return null; } textSource.setEndOffset(originalTextLength, false, layoutAwareScan); @@ -1278,7 +1212,7 @@ export class TextScanner extends EventDispatcher { sentenceBackwardQuoteMap ); - return { dictionaryEntries, sentence, type: 'terms' }; + return {dictionaryEntries, sentence, type: 'terms'}; } /** @@ -1310,7 +1244,7 @@ export class TextScanner extends EventDispatcher { sentenceBackwardQuoteMap ); - return { dictionaryEntries, sentence, type: 'kanji' }; + return {dictionaryEntries, sentence, type: 'kanji'}; } /** @@ -1382,7 +1316,7 @@ export class TextScanner extends EventDispatcher { */ async _searchAtFromTouchStart(x, y, inputInfo) { const textSourceCurrentPrevious = this._textSourceCurrent !== null ? this._textSourceCurrent.clone() : null; - const { input } = inputInfo; + const {input} = inputInfo; const preventScroll = input !== null && input.preventTouchScrolling; await this._searchAt(x, y, inputInfo); @@ -1417,7 +1351,7 @@ export class TextScanner extends EventDispatcher { const inputInfo = this._getMatchingInputGroupFromEvent('pen', eventType, e); if (inputInfo === null) { return; } - const { input } = inputInfo; + const {input} = inputInfo; if (input === null || !this._isPenEventSupported(eventType, input)) { return; } const preventScroll = input !== null && input.preventPenScrolling; @@ -1483,7 +1417,7 @@ export class TextScanner extends EventDispatcher { const modifiersSet = new Set(modifiers); for (let i = 0, ii = this._inputs.length; i < ii; ++i) { const input = this._inputs[i]; - const { include, exclude, types } = input; + const {include, exclude, types} = input; if (!types.has(pointerType)) { continue; } if (this._setHasAll(modifiersSet, include) && (exclude.length === 0 || !this._setHasAll(modifiersSet, exclude))) { if (include.length > 0) { @@ -1496,8 +1430,8 @@ export class TextScanner extends EventDispatcher { return ( fallbackIndex >= 0 ? - this._createInputInfo(this._inputs[fallbackIndex], pointerType, eventType, true, modifiers, modifierKeys) : - null + this._createInputInfo(this._inputs[fallbackIndex], pointerType, eventType, true, modifiers, modifierKeys) : + null ); } @@ -1512,7 +1446,7 @@ export class TextScanner extends EventDispatcher { * @returns {import('text-scanner').InputInfo} */ _createInputInfo(input, pointerType, eventType, passive, modifiers, modifierKeys, detail) { - return { input, pointerType, eventType, passive, modifiers, modifierKeys, detail }; + return {input, pointerType, eventType, passive, modifiers, modifierKeys, detail}; } /** @@ -1534,7 +1468,7 @@ export class TextScanner extends EventDispatcher { * @returns {import('text-scanner').InputConfig} */ _convertInput(input) { - const { options } = input; + const {options} = input; return { include: this._getInputArray(input.include), exclude: this._getInputArray(input.exclude), @@ -1561,8 +1495,8 @@ export class TextScanner extends EventDispatcher { _getInputArray(value) { return ( typeof value === 'string' ? - value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0) : - [] + value.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0) : + [] ); } @@ -1570,7 +1504,7 @@ export class TextScanner extends EventDispatcher { * @param {{mouse: boolean, touch: boolean, pen: boolean}} details * @returns {Set<'mouse'|'touch'|'pen'>} */ - _getInputTypeSet({ mouse, touch, pen }) { + _getInputTypeSet({mouse, touch, pen}) { const set = new Set(); if (mouse) { set.add('mouse'); } if (touch) { set.add('touch'); } @@ -1642,14 +1576,14 @@ export class TextScanner extends EventDispatcher { ranges.push(range.cloneRange()); } } - return { ranges }; + return {ranges}; } /** * @param {import('text-scanner').SelectionRestoreInfo} selectionRestoreInfo */ _restoreSelection(selectionRestoreInfo) { - const { ranges } = selectionRestoreInfo; + const {ranges} = selectionRestoreInfo; const selection = window.getSelection(); if (selection === null) { return; } selection.removeAllRanges(); @@ -1666,7 +1600,7 @@ export class TextScanner extends EventDispatcher { * @param {string} reason */ _triggerClear(reason) { - this.trigger('clear', { reason }); + this.trigger('clear', {reason}); } /** diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 3647e2cb62..aa1b71dd57 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -1,5 +1,4 @@ /* - * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * Copyright (C) 2016-2022 Yomichan Authors * @@ -197,8 +196,8 @@ export class Translator { return results; } - /* https://github.com/seth-js/yomichan-de */ // Find terms internal implementation + /** * @param {string} text * @param {Map} enabledDictionaryMap @@ -214,187 +213,20 @@ export class Translator { return {dictionaryEntries: [], originalTextLength: 0}; } - // Makes it so that Yomichan doesn't look up parts of a word, only the full word - // Ex: находится would give me definitions for на - // Also chop the text since words with newlines or spaces after is bugging out - - // const deinflections = await this._findTermsInternal2(text, enabledDictionaryMap, options); - - const choppedText = text.replace(/\n/g, ' ').trim(); - - let deinflections = await this._findTermsInternal2(choppedText, enabledDictionaryMap, options); - - /** - * @type {import("translation-internal").DatabaseDeinflection[]} - */ - const filteredDeinflections = []; - - let smallestMatch = ''; - - deinflections.forEach((flect) => { - const {originalText} = flect; - - if (!/\s/.test(originalText) - && /\p{L}$/u.test(originalText) - && !smallestMatch) { smallestMatch = originalText; } - }); - - deinflections.forEach((flect) => { - const {originalText, databaseEntries} = flect; - - if (databaseEntries && databaseEntries.length > 0) { - if (!smallestMatch.includes(originalText) || smallestMatch === originalText) { - filteredDeinflections.push(flect); - } - } - }); - - deinflections = [...filteredDeinflections]; - - // Automatically handle non-lemma forms by looking up what they point to - const requiredSearches = {}; - let searching = false; - - do { - searching = false; - - for (const {databaseEntries} of deinflections) { - for (const ent of databaseEntries) { - const {definitionTags, definitions, term} = ent; - - if (definitionTags.includes('non-lemma')) { - ent.skip = true; - - for (const definition of definitions) { - const lemma = (typeof definition === 'string') ? definition.replace(/.+?\(->(?=.+?\)$)/, '').replace(/\)$/, '') : ''; - const reason = (typeof definition === 'string') ? definition.replace(/\s\(->.+/, '') : ''; - - if (!requiredSearches[lemma]) { - searching = true; - requiredSearches[lemma] = {form: term, reasons: [reason]}; - } else if (!requiredSearches[lemma].reasons.includes(reason)) { - searching = true; - requiredSearches[lemma].reasons.push(reason); - } - } - } - } - } - - const extraDeinflections = []; - - for (const [lemma, {form, reasons}] of Object.entries(requiredSearches)) { - const flections = await this._findTermsInternal2(lemma, enabledDictionaryMap, options); - - /** - * @type {import("translation-internal").DatabaseDeinflection[]} - */ - const filteredFlections = []; - - let innerSmallestMatch = ''; - - flections.forEach((flect) => { - const {originalText} = flect; - - if (!/\s/.test(originalText) - && /\p{L}$/u.test(originalText) - && !innerSmallestMatch) { innerSmallestMatch = originalText; } - }); - - flections.forEach((flect) => { - const {originalText, deinflectedText, databaseEntries} = flect; - - if (databaseEntries && databaseEntries.length > 0 && lemma === deinflectedText) { - if (!innerSmallestMatch.includes(originalText) || innerSmallestMatch === originalText) { - filteredFlections.push(flect); - } - } - }); - - for (const flect of filteredFlections) { - const {databaseEntries} = flect; - - flect.originalText = form; - - databaseEntries.forEach((ent) => { - const {definitionTags} = ent; - - if (definitionTags.includes('non-lemma')) { ent.skip = true; } - }); - - reasons.forEach((/** @type {string} */ reason) => { - flect.reasons.push(reason); - }); - - flect.isExtra = true; - extraDeinflections.push(flect); - } - } - - if (extraDeinflections.length > 0) { deinflections.push(...extraDeinflections); } - } while (searching); + const deinflections = await this._findTermsInternal2(text, enabledDictionaryMap, options); let originalTextLength = 0; const dictionaryEntries = []; const ids = new Set(); - - // Added the isExtra variable so it can be checked - - // for (const {databaseEntries, originalText, transformedText, deinflectedText, reasons} of deinflections) { - - const uniqueResultsObj = {}; - const uniqueResults = []; - - for (const {databaseEntries, originalText, transformedText, deinflectedText, reasons, isExtra} of deinflections) { - if (!uniqueResultsObj[deinflectedText]) { uniqueResultsObj[deinflectedText] = {}; } - if (!uniqueResultsObj[deinflectedText][originalText]) { uniqueResultsObj[deinflectedText][originalText] = {databaseEntries, originalText, transformedText, deinflectedText, isExtra}; } - - if (reasons.length > 0) { - uniqueResultsObj[deinflectedText][originalText].reasons = [...reasons]; - } else if (!uniqueResultsObj[deinflectedText][originalText].reasons) { - uniqueResultsObj[deinflectedText][originalText].reasons = []; - } - } - - for (const {originalText, deinflectedText} of deinflections) { - if (originalText === deinflectedText && Object.entries(uniqueResultsObj[deinflectedText]).length > 1) { - delete uniqueResultsObj[deinflectedText][originalText]; - } - } - - for (const [lemma, info] of Object.entries(uniqueResultsObj)) { - const [[surface, {databaseEntries, transformedText, reasons, isExtra}]] = Object.entries(info); - uniqueResults.push({databaseEntries, originalText: surface, transformedText, deinflectedText: lemma, reasons, isExtra}); - } - - // console.log(uniqueResults); - - for (const {databaseEntries, originalText, transformedText, deinflectedText, reasons, isExtra} of uniqueResults) { + for (const {databaseEntries, originalText, transformedText, deinflectedText, reasons} of deinflections) { if (databaseEntries.length === 0) { continue; } - - // Makes it so that the character length of lemmas don't affect the non-lemma match - // originalTextLength = Math.max(originalTextLength, originalText.length); - - if (!isExtra) { - originalTextLength = Math.max(originalTextLength, originalText.length); - } - + originalTextLength = Math.max(originalTextLength, originalText.length); for (const databaseEntry of databaseEntries) { const {id} = databaseEntry; if (ids.has(id)) { continue; } - - // Makes it so that non-lemma entries aren't added to the dictionary entries - // We already have what they point to and the relevant form info - - // const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, true, enabledDictionaryMap); - // dictionaryEntries.push(dictionaryEntry); - // ids.add(id); - - if (!databaseEntry.skip) { - const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, true, enabledDictionaryMap, tagAggregator); - dictionaryEntries.push(dictionaryEntry); - ids.add(id); - } + const dictionaryEntry = this._createTermDictionaryEntryFromDatabaseEntry(databaseEntry, originalText, transformedText, deinflectedText, reasons, true, enabledDictionaryMap, tagAggregator); + dictionaryEntries.push(dictionaryEntry); + ids.add(id); } } @@ -410,8 +242,8 @@ export class Translator { async _findTermsInternal2(text, enabledDictionaryMap, options) { const deinflections = ( options.deinflect ? - this._getAllDeinflections(text, options) : - [this._createDeinflection(text, text, text, 0, [])] + this._getAllDeinflections(text, options) : + [this._createDeinflection(text, text, text, 0, [])] ); if (deinflections.length === 0) { return []; } @@ -528,7 +360,7 @@ export class Translator { const jp = this._japaneseUtil; let length = 0; for (const c of text) { - if (!jp.isCodePointJapanese(/** @type {number} */(c.codePointAt(0)))) { + if (!jp.isCodePointJapanese(/** @type {number} */ (c.codePointAt(0)))) { return text.substring(0, length); } length += c.length; @@ -583,7 +415,7 @@ export class Translator { * @returns {import('translation-internal').DatabaseDeinflection} */ _createDeinflection(originalText, transformedText, deinflectedText, rules, reasons) { - return {originalText, transformedText, deinflectedText, rules, reasons, databaseEntries: [], isExtra: false}; + return {originalText, transformedText, deinflectedText, rules, reasons, databaseEntries: []}; } // Term dictionary entry grouping @@ -2026,8 +1858,8 @@ export class Translator { } dictionaryEntry.frequencyOrder = ( frequencyMin <= frequencyMax ? - (ascending ? frequencyMin : -frequencyMax) : - (ascending ? Number.MAX_SAFE_INTEGER : 0) + (ascending ? frequencyMin : -frequencyMax) : + (ascending ? Number.MAX_SAFE_INTEGER : 0) ); for (const definition of definitions) { frequencyMin = Number.MAX_SAFE_INTEGER; @@ -2041,8 +1873,8 @@ export class Translator { } definition.frequencyOrder = ( frequencyMin <= frequencyMax ? - (ascending ? frequencyMin : -frequencyMax) : - (ascending ? Number.MAX_SAFE_INTEGER : 0) + (ascending ? frequencyMin : -frequencyMax) : + (ascending ? Number.MAX_SAFE_INTEGER : 0) ); } frequencyMap.clear(); diff --git a/ext/search.html b/ext/search.html index e4ad7d978d..8c595cc4e3 100644 --- a/ext/search.html +++ b/ext/search.html @@ -49,8 +49,7 @@

Yomitan Search

- - +
diff --git a/types/ext/dictionary-database.d.ts b/types/ext/dictionary-database.d.ts index 1796f957a7..6569f76bf5 100644 --- a/types/ext/dictionary-database.d.ts +++ b/types/ext/dictionary-database.d.ts @@ -1,5 +1,4 @@ /* - * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * * This program is free software: you can redistribute it and/or modify @@ -58,7 +57,6 @@ export type DatabaseTermEntry = { export type DatabaseTermEntryWithId = DatabaseTermEntry & DatabaseId; export type TermEntry = { - skip: boolean; index: number; matchType: MatchType; matchSource: MatchSource; diff --git a/types/ext/translation-internal.d.ts b/types/ext/translation-internal.d.ts index d6cd32b840..784a597983 100644 --- a/types/ext/translation-internal.d.ts +++ b/types/ext/translation-internal.d.ts @@ -1,5 +1,4 @@ /* - * Copyright (C) 2023 Scrub Caffeinated * Copyright (C) 2023 Yomitan Authors * * This program is free software: you can redistribute it and/or modify @@ -63,5 +62,4 @@ export type DatabaseDeinflection = { rules: DeinflectionRuleFlags; reasons: string[]; databaseEntries: DictionaryDatabase.TermEntry[]; - isExtra: boolean; }; From 2ca648cb63e7ca0fb7a218160a85a6a8076ae390 Mon Sep 17 00:00:00 2001 From: Cashew Date: Mon, 11 Dec 2023 04:48:12 +0900 Subject: [PATCH 10/12] Lint error fix --- dev/build-libs.js | 4 ++-- dev/dictionary-validate.js | 10 +++++----- dev/generate-css-json.js | 6 +++--- dev/translator-vm.js | 10 +++++----- ext/js/data/database.js | 12 ++++++------ ext/js/language/deinflector.js | 2 +- ext/js/language/dictionary-database.js | 14 +++++++------- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/dev/build-libs.js b/dev/build-libs.js index 789849fc56..e1135fc3ed 100644 --- a/dev/build-libs.js +++ b/dev/build-libs.js @@ -45,8 +45,8 @@ async function buildLib(scriptPath) { }); } -/** - * Bundles libraries. +/** + * Bundles libraries. */ export async function buildLibs() { const devLibPath = path.join(dirname, 'lib'); diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js index 96195b74ef..6778f2eae9 100644 --- a/dev/dictionary-validate.js +++ b/dev/dictionary-validate.js @@ -72,9 +72,9 @@ async function validateDictionaryBanks(mode, zip, fileNameFormat, schema) { /** * Validates a dictionary. - * @param {import('dev/schema-validate').ValidateMode} mode - Mode of validation. - * @param {import('jszip')} archive - Zip archive of the dictionary. - * @param {import('dev/dictionary-validate').Schemas} schemas - Schema to use for validation. + * @param {import('dev/schema-validate').ValidateMode} mode Mode of validation. + * @param {import('jszip')} archive Zip archive of the dictionary. + * @param {import('dev/dictionary-validate').Schemas} schemas Schema to use for validation. */ export async function validateDictionary(mode, archive, schemas) { const fileName = 'index.json'; @@ -121,8 +121,8 @@ export function getSchemas() { /** * Validates dictionary files and logs the results to the console. - * @param {import('dev/schema-validate').ValidateMode} mode - Mode of validation. - * @param {string[]} dictionaryFileNames - Dictionary file names. + * @param {import('dev/schema-validate').ValidateMode} mode Mode of validation. + * @param {string[]} dictionaryFileNames Dictionary file names. */ export async function testDictionaryFiles(mode, dictionaryFileNames) { const schemas = getSchemas(); diff --git a/dev/generate-css-json.js b/dev/generate-css-json.js index 2b456a491d..a003534695 100644 --- a/dev/generate-css-json.js +++ b/dev/generate-css-json.js @@ -87,7 +87,7 @@ function removeProperty(styles, property, removedProperties) { /** * Manually formats JSON for improved compactness. - * @param {import('css-style-applier').RawStyleData} rules - CSS ruleset. + * @param {import('css-style-applier').RawStyleData} rules CSS ruleset. * @returns {string} */ export function formatRulesJson(rules) { @@ -124,8 +124,8 @@ export function formatRulesJson(rules) { /** * Generates a CSS ruleset. - * @param {string} cssFile - Path to CSS file. - * @param {string} overridesCssFile - Path to override CSS file. + * @param {string} cssFile Path to CSS file. + * @param {string} overridesCssFile Path to override CSS file. * @returns {import('css-style-applier').RawStyleData} * @throws {Error} */ diff --git a/dev/translator-vm.js b/dev/translator-vm.js index 371fb67f34..d2b023b605 100644 --- a/dev/translator-vm.js +++ b/dev/translator-vm.js @@ -45,7 +45,7 @@ export class TranslatorVM { } } }; - // @ts-expect-error - Overwriting a global + // @ts-expect-error Overwriting a global global.chrome = chrome; /** @type {?JapaneseUtil} */ @@ -58,9 +58,9 @@ export class TranslatorVM { this._dictionaryName = null; } - /** + /** * Returns this VM's translator. - * @type {Translator} + * @type {Translator} */ get translator() { if (this._translator === null) { throw new Error('Not prepared'); } @@ -69,8 +69,8 @@ export class TranslatorVM { /** * Initialize this translator VM from a dictionary. - * @param {string} dictionaryDirectory - Directory of the dictionary files. - * @param {string} dictionaryName - Name of the dictionary. + * @param {string} dictionaryDirectory Directory of the dictionary files. + * @param {string} dictionaryName Name of the dictionary. */ async prepare(dictionaryDirectory, dictionaryName) { // Dictionary diff --git a/ext/js/data/database.js b/ext/js/data/database.js index 557a625b84..d3be43ddd2 100644 --- a/ext/js/data/database.js +++ b/ext/js/data/database.js @@ -97,11 +97,11 @@ export class Database { /** * Add items in bulk to the object store. - * *count* items will be added beginning from *start* index of *items* list. + * count items will be added beginning from start index of items list. * @param {TObjectStoreName} objectStoreName - * @param {unknown[]} items - List of items to add. - * @param {number} start - Start index. Added items begin at items[start]. - * @param {number} count - Count of items to add. + * @param {unknown[]} items List of items to add. + * @param {number} start Start index. Added items begin at items[start]. + * @param {number} count Count of items to add. * @returns {Promise} */ bulkAdd(objectStoreName, items, start, count) { @@ -301,8 +301,8 @@ export class Database { } /** - * Attempts to delete the named database. - * If the database already exists and there are open connections that don't close in response to a versionchange event, the request will be blocked until all they close. + * Attempts to delete the named database. + * If the database already exists and there are open connections that don't close in response to a versionchange event, the request will be blocked until all they close. * If the request is successful request's result will be null. * @param {string} databaseName * @returns {Promise} diff --git a/ext/js/language/deinflector.js b/ext/js/language/deinflector.js index 0362123492..b7fba6073c 100644 --- a/ext/js/language/deinflector.js +++ b/ext/js/language/deinflector.js @@ -35,7 +35,7 @@ export class Deinflector { /** * Deinflects a Japanese term to all of its possible dictionary forms. - * @param {string} source - The source term to deinflect. + * @param {string} source The source term to deinflect. * @returns {import('translation-internal').Deinflection[]} * @example * const deinflector = new Deinflector(deinflectionReasons); diff --git a/ext/js/language/dictionary-database.js b/ext/js/language/dictionary-database.js index 1e15ff4214..ce5041c827 100644 --- a/ext/js/language/dictionary-database.js +++ b/ext/js/language/dictionary-database.js @@ -231,9 +231,9 @@ export class DictionaryDatabase { /** * Find terms in bulk. - * @param {string[]} termList - The list of terms to find. - * @param {import('dictionary-database').DictionarySet} dictionaries - Dictionaries to find the terms from. - * @param {import('dictionary-database').MatchType} matchType - Matching type. + * @param {string[]} termList The list of terms to find. + * @param {import('dictionary-database').DictionarySet} dictionaries Dictionaries to find the terms from. + * @param {import('dictionary-database').MatchType} matchType Matching type. * @returns {Promise} */ findTermsBulk(termList, dictionaries, matchType) { @@ -266,8 +266,8 @@ export class DictionaryDatabase { /** * Find exact terms in bulk. - * @param {import('dictionary-database').TermExactRequest[]} termList - The list of terms to find. - * @param {import('dictionary-database').DictionarySet} dictionaries - Dictionaries to find the term from. + * @param {import('dictionary-database').TermExactRequest[]} termList The list of terms to find. + * @param {import('dictionary-database').DictionarySet} dictionaries Dictionaries to find the term from. * @returns {Promise} */ findTermsExactBulk(termList, dictionaries) { @@ -301,8 +301,8 @@ export class DictionaryDatabase { /** * Find kanji in bulk. - * @param {string[]} kanjiList - The list of kanji to find. - * @param {import('dictionary-database').DictionarySet} dictionaries - Dictionaries to find from. + * @param {string[]} kanjiList The list of kanji to find. + * @param {import('dictionary-database').DictionarySet} dictionaries Dictionaries to find from. * @returns {Promise} */ findKanjiBulk(kanjiList, dictionaries) { From 0411cc0119c9d4e4fe85fc1ab332d82c840d8def Mon Sep 17 00:00:00 2001 From: Cashew Date: Mon, 11 Dec 2023 04:53:11 +0900 Subject: [PATCH 11/12] Lint error fix --- dev/translator-vm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/translator-vm.js b/dev/translator-vm.js index d2b023b605..9a4923183f 100644 --- a/dev/translator-vm.js +++ b/dev/translator-vm.js @@ -45,7 +45,7 @@ export class TranslatorVM { } } }; - // @ts-expect-error Overwriting a global + // @ts-expect-error - Overwriting a global global.chrome = chrome; /** @type {?JapaneseUtil} */ From c10a9afc9d8bb135b923f93f1983312835c67b7e Mon Sep 17 00:00:00 2001 From: Cashew Date: Tue, 19 Dec 2023 13:24:25 +0900 Subject: [PATCH 12/12] update JSDoc comments --- dev/dictionary-validate.js | 12 +++++------ dev/generate-css-json.js | 4 ++-- ext/js/data/anki-note-builder.js | 28 -------------------------- ext/js/data/database.js | 11 ++++------ ext/js/language/deinflector.js | 6 +----- ext/js/language/dictionary-database.js | 28 +++++++------------------- 6 files changed, 20 insertions(+), 69 deletions(-) diff --git a/dev/dictionary-validate.js b/dev/dictionary-validate.js index 6778f2eae9..7842c65e99 100644 --- a/dev/dictionary-validate.js +++ b/dev/dictionary-validate.js @@ -71,10 +71,10 @@ async function validateDictionaryBanks(mode, zip, fileNameFormat, schema) { } /** - * Validates a dictionary. - * @param {import('dev/schema-validate').ValidateMode} mode Mode of validation. - * @param {import('jszip')} archive Zip archive of the dictionary. - * @param {import('dev/dictionary-validate').Schemas} schemas Schema to use for validation. + * Validates a dictionary from its zip archive. + * @param {import('dev/schema-validate').ValidateMode} mode + * @param {import('jszip')} archive + * @param {import('dev/dictionary-validate').Schemas} schemas */ export async function validateDictionary(mode, archive, schemas) { const fileName = 'index.json'; @@ -121,8 +121,8 @@ export function getSchemas() { /** * Validates dictionary files and logs the results to the console. - * @param {import('dev/schema-validate').ValidateMode} mode Mode of validation. - * @param {string[]} dictionaryFileNames Dictionary file names. + * @param {import('dev/schema-validate').ValidateMode} mode + * @param {string[]} dictionaryFileNames */ export async function testDictionaryFiles(mode, dictionaryFileNames) { const schemas = getSchemas(); diff --git a/dev/generate-css-json.js b/dev/generate-css-json.js index a003534695..ac0859a71b 100644 --- a/dev/generate-css-json.js +++ b/dev/generate-css-json.js @@ -124,8 +124,8 @@ export function formatRulesJson(rules) { /** * Generates a CSS ruleset. - * @param {string} cssFile Path to CSS file. - * @param {string} overridesCssFile Path to override CSS file. + * @param {string} cssFile + * @param {string} overridesCssFile * @returns {import('css-style-applier').RawStyleData} * @throws {Error} */ diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index 80cc210a53..9240c1d879 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -22,16 +22,10 @@ import {TemplateRendererProxy} from '../templates/template-renderer-proxy.js'; import {yomitan} from '../yomitan.js'; import {AnkiUtil} from './anki-util.js'; -/** - * Anki Note Builder Class. - */ export class AnkiNoteBuilder { /** * Initiate an instance of AnkiNoteBuilder. * @param {{japaneseUtil: import('../language/sandbox/japanese-util.js').JapaneseUtil}} details - * @example - * const japaneseUtil = new JapaneseUtil(null); - * const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil}); */ constructor({japaneseUtil}) { /** @type {import('../language/sandbox/japanese-util.js').JapaneseUtil} */ @@ -47,30 +41,8 @@ export class AnkiNoteBuilder { } /** - * Creates an Anki note. * @param {import('anki-note-builder').CreateNoteDetails} details * @returns {Promise} - * @example - * const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil}); - * const details = { - * dictionaryEntry, - * mode: 'test', - * context, - * template, - * deckName: 'deckName', - * modelName: 'modelName', - * fields, - * tags: ['yomitan'], - * checkForDuplicates: true, - * duplicateScope: 'collection', - * duplicateScopeCheckAllModels: false, - * resultOutputMode: mode, - * glossaryLayoutMode: 'default', - * compactTags: false, - * requirements: [], - * mediaOptions: null - * }; - * const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote(details); */ async createNote({ dictionaryEntry, diff --git a/ext/js/data/database.js b/ext/js/data/database.js index cb09a680c4..cd9aff15b1 100644 --- a/ext/js/data/database.js +++ b/ext/js/data/database.js @@ -17,7 +17,6 @@ */ /** - * Database class to store objects. * @template {string} TObjectStoreName */ export class Database { @@ -29,7 +28,6 @@ export class Database { } /** - * Opens the DB. * @param {string} databaseName * @param {number} version * @param {import('database').StructureDefinition[]} structure @@ -53,7 +51,6 @@ export class Database { } /** - * Closes the DB. * @throws {Error} */ close() { @@ -66,7 +63,7 @@ export class Database { } /** - * Returns true if DB opening is in process. + * Returns true if the database opening is in process. * @returns {boolean} */ isOpening() { @@ -74,7 +71,7 @@ export class Database { } /** - * Returns true if the DB is open. + * Returns true if the database is fully opened. * @returns {boolean} */ isOpen() { @@ -97,10 +94,10 @@ export class Database { /** * Add items in bulk to the object store. - * count items will be added beginning from start index of items list. + * _count_ items will be added, starting from _start_ index of _items_ list. * @param {TObjectStoreName} objectStoreName * @param {unknown[]} items List of items to add. - * @param {number} start Start index. Added items begin at items[start]. + * @param {number} start Start index. Added items begin at _items_[_start_]. * @param {number} count Count of items to add. * @returns {Promise} */ diff --git a/ext/js/language/deinflector.js b/ext/js/language/deinflector.js index 90ca79ead4..676f45a197 100644 --- a/ext/js/language/deinflector.js +++ b/ext/js/language/deinflector.js @@ -16,16 +16,13 @@ * along with this program. If not, see . */ -/** - * This class deinflects Japanese terms to its dictionary form. - */ export class Deinflector { /** * @param {import('deinflector').ReasonsRaw} reasons * @example * const deinflectionReasons = JSON.parse( * readFileSync(path.join('ext/data/deinflect.json')).toString(), - * ) as object; + * ); * const deinflector = new Deinflector(deinflectionReasons); */ constructor(reasons) { @@ -101,7 +98,6 @@ export class Deinflector { } /** - * Given a list of rules, return the corresponding deinflection rule flags. * @param {string[]} rules * @returns {import('translation-internal').DeinflectionRuleFlags} */ diff --git a/ext/js/language/dictionary-database.js b/ext/js/language/dictionary-database.js index ce5041c827..c47e1e902f 100644 --- a/ext/js/language/dictionary-database.js +++ b/ext/js/language/dictionary-database.js @@ -19,9 +19,6 @@ import {log, stringReverse} from '../core.js'; import {Database} from '../data/database.js'; -/** - * This class represents the dictionary database. - */ export class DictionaryDatabase { constructor() { /** @type {Database} */ @@ -144,7 +141,6 @@ export class DictionaryDatabase { } /** - * Purges the database. * @returns {Promise} */ async purge() { @@ -166,7 +162,6 @@ export class DictionaryDatabase { } /** - * Deletes a dictionary. * @param {string} dictionaryName * @param {number} progressRate * @param {import('dictionary-database').DeleteDictionaryProgressCallback} onProgress @@ -230,10 +225,9 @@ export class DictionaryDatabase { } /** - * Find terms in bulk. - * @param {string[]} termList The list of terms to find. - * @param {import('dictionary-database').DictionarySet} dictionaries Dictionaries to find the terms from. - * @param {import('dictionary-database').MatchType} matchType Matching type. + * @param {string[]} termList + * @param {import('dictionary-database').DictionarySet} dictionaries + * @param {import('dictionary-database').MatchType} matchType * @returns {Promise} */ findTermsBulk(termList, dictionaries, matchType) { @@ -265,9 +259,8 @@ export class DictionaryDatabase { } /** - * Find exact terms in bulk. - * @param {import('dictionary-database').TermExactRequest[]} termList The list of terms to find. - * @param {import('dictionary-database').DictionarySet} dictionaries Dictionaries to find the term from. + * @param {import('dictionary-database').TermExactRequest[]} termList + * @param {import('dictionary-database').DictionarySet} dictionaries * @returns {Promise} */ findTermsExactBulk(termList, dictionaries) { @@ -277,7 +270,6 @@ export class DictionaryDatabase { } /** - * Find terms by sequence in bulk. * @param {import('dictionary-database').DictionaryAndQueryRequest[]} items * @returns {Promise} */ @@ -288,7 +280,6 @@ export class DictionaryDatabase { } /** - * Find term meta in bulk. * @param {string[]} termList * @param {import('dictionary-database').DictionarySet} dictionaries * @returns {Promise} @@ -300,9 +291,8 @@ export class DictionaryDatabase { } /** - * Find kanji in bulk. - * @param {string[]} kanjiList The list of kanji to find. - * @param {import('dictionary-database').DictionarySet} dictionaries Dictionaries to find from. + * @param {string[]} kanjiList + * @param {import('dictionary-database').DictionarySet} dictionaries * @returns {Promise} */ findKanjiBulk(kanjiList, dictionaries) { @@ -312,7 +302,6 @@ export class DictionaryDatabase { } /** - * Find kanji meta in bulk. * @param {string[]} kanjiList * @param {import('dictionary-database').DictionarySet} dictionaries * @returns {Promise} @@ -324,7 +313,6 @@ export class DictionaryDatabase { } /** - * Find tag meta in bulk. * @param {import('dictionary-database').DictionaryAndQueryRequest[]} items * @returns {Promise<(import('dictionary-database').Tag|undefined)[]>} */ @@ -335,7 +323,6 @@ export class DictionaryDatabase { } /** - * Find tag for title. * @param {string} name * @param {string} dictionary * @returns {Promise} @@ -356,7 +343,6 @@ export class DictionaryDatabase { } /** - * Get dictionary metadata. * @returns {Promise} */ getDictionaryInfo() {