diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2020-06-28 14:59:01 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-06-28 14:59:01 -0400 | 
| commit | 5183fb575f52c1cb9c01ae4b6bf9572c2595ce04 (patch) | |
| tree | 14c0a865a07297ba5c0f71be4435e4ddf91a3227 | |
| parent | f2345b7d1c035422dad553a4541ee486d952afa9 (diff) | |
Backend refactor (#631)
* Mark fields as private
* Remove static
* Make functions private
* Create onCommand handler
* Group event handlers
* Move functions
* Merge _onOptionsUpdated and _applyOptions
* Rename event handler
* Move event handlers
* Remove _getOptionsSchema
* Move private functions
| -rw-r--r-- | ext/bg/js/backend.js | 663 | 
1 files changed, 332 insertions, 331 deletions
| diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 5ca84cab..03bf243d 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -42,38 +42,38 @@  class Backend {      constructor() { -        this.environment = new Environment(); -        this.database = new Database(); -        this.dictionaryImporter = new DictionaryImporter(); -        this.translator = new Translator(this.database); -        this.anki = new AnkiConnect(); -        this.mecab = new Mecab(); -        this.clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); -        this.options = null; -        this.optionsSchema = null; -        this.defaultAnkiFieldTemplates = null; -        this.audioUriBuilder = new AudioUriBuilder(); -        this.audioSystem = new AudioSystem({ -            audioUriBuilder: this.audioUriBuilder, +        this._environment = new Environment(); +        this._database = new Database(); +        this._dictionaryImporter = new DictionaryImporter(); +        this._translator = new Translator(this._database); +        this._anki = new AnkiConnect(); +        this._mecab = new Mecab(); +        this._clipboardMonitor = new ClipboardMonitor({getClipboard: this._onApiClipboardGet.bind(this)}); +        this._options = null; +        this._optionsSchema = null; +        this._defaultAnkiFieldTemplates = null; +        this._audioUriBuilder = new AudioUriBuilder(); +        this._audioSystem = new AudioSystem({ +            audioUriBuilder: this._audioUriBuilder,              useCache: false          }); -        this.ankiNoteBuilder = new AnkiNoteBuilder({ -            anki: this.anki, -            audioSystem: this.audioSystem, +        this._ankiNoteBuilder = new AnkiNoteBuilder({ +            anki: this._anki, +            audioSystem: this._audioSystem,              renderTemplate: this._renderTemplate.bind(this)          });          this._templateRenderer = new TemplateRenderer();          const url = (typeof window === 'object' && window !== null ? window.location.href : ''); -        this.optionsContext = {depth: 0, url}; +        this._optionsContext = {depth: 0, url}; -        this.clipboardPasteTarget = ( +        this._clipboardPasteTarget = (              typeof document === 'object' && document !== null ?              document.querySelector('#clipboard-paste-target') :              null          ); -        this.popupWindow = null; +        this._popupWindow = null;          this._isPrepared = false;          this._prepareError = false; @@ -171,32 +171,32 @@ class Backend {              yomichan.on('log', this._onLog.bind(this)); -            await this.environment.prepare(); +            await this._environment.prepare();              try { -                await this.database.prepare(); +                await this._database.prepare();              } catch (e) {                  yomichan.logError(e);              } -            await this.translator.prepare(); +            await this._translator.prepare();              await profileConditionsDescriptorPromise; -            this.optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET'); -            this.defaultAnkiFieldTemplates = (await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET')).trim(); -            this.options = await optionsLoad(); -            this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, this.options); +            this._optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET'); +            this._defaultAnkiFieldTemplates = (await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET')).trim(); +            this._options = await optionsLoad(); +            this._options = JsonSchema.getValidValueOrDefault(this._optionsSchema, this._options); -            this.onOptionsUpdated('background'); +            this._applyOptions('background'); -            const options = this.getOptions(this.optionsContext); +            const options = this.getOptions(this._optionsContext);              if (options.general.showGuide) {                  chrome.tabs.create({url: chrome.runtime.getURL('/bg/guide.html')});              } -            this.clipboardMonitor.on('change', this._onClipboardText.bind(this)); +            this._clipboardMonitor.on('change', this._onClipboardTextChange.bind(this));              this._sendMessageAllTabs('backendPrepared'); -            const callback = () => this.checkLastError(chrome.runtime.lastError); +            const callback = () => this._checkLastError(chrome.runtime.lastError);              chrome.runtime.sendMessage({action: 'backendPrepared'}, callback);          } catch (e) {              yomichan.logError(e); @@ -218,7 +218,7 @@ class Backend {      }      handleCommand(...args) { -        return this._runCommand(...args); +        return this._onCommand(...args);      }      handleZoomChange(...args) { @@ -230,24 +230,39 @@ class Backend {      }      handleMessage(...args) { -        return this.onMessage(...args); +        return this._onMessage(...args);      } -    _sendMessageAllTabs(action, params={}) { -        const callback = () => this.checkLastError(chrome.runtime.lastError); -        chrome.tabs.query({}, (tabs) => { -            for (const tab of tabs) { -                chrome.tabs.sendMessage(tab.id, {action, params}, callback); -            } -        }); +    getFullOptions(useSchema=false) { +        const options = this._options; +        return useSchema ? JsonSchema.createProxy(options, this._optionsSchema) : options;      } -    onOptionsUpdated(source) { -        this.applyOptions(); -        this._sendMessageAllTabs('optionsUpdated', {source}); +    getOptions(optionsContext, useSchema=false) { +        return this._getProfile(optionsContext, useSchema).options; +    } + +    // Event handlers + +    _onClipboardTextChange({text}) { +        this._onCommandSearch({mode: 'popup', query: text});      } -    onMessage({action, params}, sender, callback) { +    _onLog({level}) { +        const levelValue = this._getErrorLevelValue(level); +        if (levelValue <= this._getErrorLevelValue(this._logErrorLevel)) { return; } + +        this._logErrorLevel = level; +        this._updateBadge(); +    } + +    // WebExtension event handlers + +    _onCommand(command) { +        this._runCommand(command); +    } + +    _onMessage({action, params}, sender, callback) {          const messageHandler = this._messageHandlers.get(action);          if (typeof messageHandler === 'undefined') { return false; } @@ -293,7 +308,7 @@ class Backend {              let forwardPort = chrome.tabs.connect(tabId, {frameId: targetFrameId, name: `cross-frame-communication-port-${senderFrameId}`});              const cleanup = () => { -                this.checkLastError(chrome.runtime.lastError); +                this._checkLastError(chrome.runtime.lastError);                  if (forwardPort !== null) {                      forwardPort.disconnect();                      forwardPort = null; @@ -314,173 +329,16 @@ class Backend {          }      } -    _onClipboardText({text}) { -        this._onCommandSearch({mode: 'popup', query: text}); -    } -      _onZoomChange({tabId, oldZoomFactor, newZoomFactor}) { -        const callback = () => this.checkLastError(chrome.runtime.lastError); +        const callback = () => this._checkLastError(chrome.runtime.lastError);          chrome.tabs.sendMessage(tabId, {action: 'zoomChanged', params: {oldZoomFactor, newZoomFactor}}, callback);      } -    _onLog({level}) { -        const levelValue = this._getErrorLevelValue(level); -        if (levelValue <= this._getErrorLevelValue(this._logErrorLevel)) { return; } - -        this._logErrorLevel = level; -        this._updateBadge(); -    } - -    applyOptions() { -        const options = this.getOptions(this.optionsContext); -        this._updateBadge(); - -        this.anki.setServer(options.anki.server); -        this.anki.setEnabled(options.anki.enable); - -        if (options.parsing.enableMecabParser) { -            this.mecab.startListener(); -        } else { -            this.mecab.stopListener(); -        } - -        if (options.general.enableClipboardPopups) { -            this.clipboardMonitor.start(); -        } else { -            this.clipboardMonitor.stop(); -        } -    } - -    getOptionsSchema() { -        return this.optionsSchema; -    } - -    getFullOptions(useSchema=false) { -        const options = this.options; -        return useSchema ? JsonSchema.createProxy(options, this.optionsSchema) : options; -    } - -    getOptions(optionsContext, useSchema=false) { -        return this.getProfile(optionsContext, useSchema).options; -    } - -    getProfile(optionsContext, useSchema=false) { -        const options = this.getFullOptions(useSchema); -        const profiles = options.profiles; -        if (typeof optionsContext.index === 'number') { -            return profiles[optionsContext.index]; -        } -        const profile = this.getProfileFromContext(options, optionsContext); -        return profile !== null ? profile : options.profiles[options.profileCurrent]; -    } - -    getProfileFromContext(options, optionsContext) { -        for (const profile of options.profiles) { -            const conditionGroups = profile.conditionGroups; -            if (conditionGroups.length > 0 && Backend.testConditionGroups(conditionGroups, optionsContext)) { -                return profile; -            } -        } -        return null; -    } - -    static testConditionGroups(conditionGroups, data) { -        if (conditionGroups.length === 0) { return false; } - -        for (const conditionGroup of conditionGroups) { -            const conditions = conditionGroup.conditions; -            if (conditions.length > 0 && Backend.testConditions(conditions, data)) { -                return true; -            } -        } - -        return false; -    } - -    static testConditions(conditions, data) { -        for (const condition of conditions) { -            if (!conditionsTestValue(profileConditionsDescriptor, condition.type, condition.operator, condition.value, data)) { -                return false; -            } -        } -        return true; -    } - -    checkLastError() { -        // NOP -    } - -    _runCommand(command, params) { -        const handler = this._commandHandlers.get(command); -        if (typeof handler !== 'function') { return false; } - -        handler(params); -        return true; -    } - -    async importDictionary(archiveSource, onProgress, details) { -        return await this.dictionaryImporter.import(this.database, archiveSource, onProgress, details); -    } - -    async _textParseScanning(text, options) { -        const results = []; -        while (text.length > 0) { -            const term = []; -            const [definitions, sourceLength] = await this.translator.findTerms( -                'simple', -                text.substring(0, options.scanning.length), -                {}, -                options -            ); -            if (definitions.length > 0 && sourceLength > 0) { -                dictTermsSort(definitions); -                const {expression, reading} = definitions[0]; -                const source = text.substring(0, sourceLength); -                for (const {text: text2, furigana} of jp.distributeFuriganaInflected(expression, reading, source)) { -                    const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode); -                    term.push({text: text2, reading: reading2}); -                } -                text = text.substring(source.length); -            } else { -                const reading = jp.convertReading(text[0], '', options.parsing.readingMode); -                term.push({text: text[0], reading}); -                text = text.substring(1); -            } -            results.push(term); -        } -        return results; -    } - -    async _textParseMecab(text, options) { -        const results = []; -        const rawResults = await this.mecab.parseText(text); -        for (const [mecabName, parsedLines] of Object.entries(rawResults)) { -            const result = []; -            for (const parsedLine of parsedLines) { -                for (const {expression, reading, source} of parsedLine) { -                    const term = []; -                    for (const {text: text2, furigana} of jp.distributeFuriganaInflected( -                        expression.length > 0 ? expression : source, -                        jp.convertKatakanaToHiragana(reading), -                        source -                    )) { -                        const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode); -                        term.push({text: text2, reading: reading2}); -                    } -                    result.push(term); -                } -                result.push([{text: '\n', reading: ''}]); -            } -            results.push([mecabName, result]); -        } -        return results; -    } -      // Message handlers      _onApiYomichanCoreReady(_params, sender) {          // tab ID isn't set in background (e.g. browser_action) -        const callback = () => this.checkLastError(chrome.runtime.lastError); +        const callback = () => this._checkLastError(chrome.runtime.lastError);          const data = {action: 'backendPrepared'};          if (typeof sender.tab === 'undefined') {              chrome.runtime.sendMessage(data, callback); @@ -492,7 +350,7 @@ class Backend {      }      _onApiOptionsSchemaGet() { -        return this.getOptionsSchema(); +        return this._optionsSchema;      }      _onApiOptionsGet({optionsContext}) { @@ -506,12 +364,12 @@ class Backend {      async _onApiOptionsSave({source}) {          const options = this.getFullOptions();          await optionsSave(options); -        this.onOptionsUpdated(source); +        this._applyOptions(source);      }      async _onApiKanjiFind({text, optionsContext}) {          const options = this.getOptions(optionsContext); -        const definitions = await this.translator.findKanji(text, options); +        const definitions = await this._translator.findKanji(text, options);          definitions.splice(options.general.maxResults);          return definitions;      } @@ -519,7 +377,7 @@ class Backend {      async _onApiTermsFind({text, details, optionsContext}) {          const options = this.getOptions(optionsContext);          const mode = options.general.resultOutputMode; -        const [definitions, length] = await this.translator.findTerms(mode, text, details, options); +        const [definitions, length] = await this._translator.findTerms(mode, text, details, options);          definitions.splice(options.general.maxResults);          return {length, definitions};      } @@ -557,7 +415,7 @@ class Backend {          if (mode !== 'kanji') {              const {customSourceUrl} = options.audio; -            await this.ankiNoteBuilder.injectAudio( +            await this._ankiNoteBuilder.injectAudio(                  definition,                  options.anki.terms.fields,                  options.audio.sources, @@ -566,15 +424,15 @@ class Backend {          }          if (details && details.screenshot) { -            await this.ankiNoteBuilder.injectScreenshot( +            await this._ankiNoteBuilder.injectScreenshot(                  definition,                  options.anki.terms.fields,                  details.screenshot              );          } -        const note = await this.ankiNoteBuilder.createNote(definition, mode, context, options, templates); -        return this.anki.addNote(note); +        const note = await this._ankiNoteBuilder.createNote(definition, mode, context, options, templates); +        return this._anki.addNote(note);      }      async _onApiDefinitionsAddable({definitions, modes, context, optionsContext}) { @@ -586,14 +444,14 @@ class Backend {              const notePromises = [];              for (const definition of definitions) {                  for (const mode of modes) { -                    const notePromise = this.ankiNoteBuilder.createNote(definition, mode, context, options, templates); +                    const notePromise = this._ankiNoteBuilder.createNote(definition, mode, context, options, templates);                      notePromises.push(notePromise);                  }              }              const notes = await Promise.all(notePromises);              const cannotAdd = []; -            const results = await this.anki.canAddNotes(notes); +            const results = await this._anki.canAddNotes(notes);              for (let resultBase = 0; resultBase < results.length; resultBase += modes.length) {                  const state = {};                  for (let modeOffset = 0; modeOffset < modes.length; ++modeOffset) { @@ -610,7 +468,7 @@ class Backend {              }              if (cannotAdd.length > 0) { -                const noteIdsArray = await this.anki.findNoteIds(cannotAdd.map((e) => e[0]), options.anki.duplicateScope); +                const noteIdsArray = await this._anki.findNoteIds(cannotAdd.map((e) => e[0]), options.anki.duplicateScope);                  for (let i = 0, ii = Math.min(cannotAdd.length, noteIdsArray.length); i < ii; ++i) {                      const noteIds = noteIdsArray[i];                      if (noteIds.length > 0) { @@ -626,7 +484,7 @@ class Backend {      }      async _onApiNoteView({noteId}) { -        return await this.anki.guiBrowse(`nid:${noteId}`); +        return await this._anki.guiBrowse(`nid:${noteId}`);      }      async _onApiTemplateRender({template, data}) { @@ -638,7 +496,7 @@ class Backend {      }      async _onApiAudioGetUri({definition, source, details}) { -        return await this.audioUriBuilder.getUri(definition, source, details); +        return await this._audioUriBuilder.getUri(definition, source, details);      }      _onApiScreenshotGet({options}, sender) { @@ -658,7 +516,7 @@ class Backend {          }          const tabId = sender.tab.id; -        const callback = () => this.checkLastError(chrome.runtime.lastError); +        const callback = () => this._checkLastError(chrome.runtime.lastError);          chrome.tabs.sendMessage(tabId, {action, params}, {frameId}, callback);          return true;      } @@ -669,7 +527,7 @@ class Backend {          }          const tabId = sender.tab.id; -        const callback = () => this.checkLastError(chrome.runtime.lastError); +        const callback = () => this._checkLastError(chrome.runtime.lastError);          chrome.tabs.sendMessage(tabId, {action, params}, callback);          return true;      } @@ -727,7 +585,7 @@ class Backend {      }      _onApiGetEnvironmentInfo() { -        return this.environment.getInfo(); +        return this._environment.getInfo();      }      async _onApiClipboardGet() { @@ -743,11 +601,11 @@ class Backend {                being an extension with clipboard permissions. It effectively asks for the                non-extension permission for clipboard access.          */ -        const {browser} = this.environment.getInfo(); +        const {browser} = this._environment.getInfo();          if (browser === 'firefox' || browser === 'firefox-mobile') {              return await navigator.clipboard.readText();          } else { -            const clipboardPasteTarget = this.clipboardPasteTarget; +            const clipboardPasteTarget = this._clipboardPasteTarget;              if (clipboardPasteTarget === null) {                  throw new Error('Reading the clipboard is not supported in this context');              } @@ -798,36 +656,36 @@ class Backend {      }      _onApiGetDefaultAnkiFieldTemplates() { -        return this.defaultAnkiFieldTemplates; +        return this._defaultAnkiFieldTemplates;      }      async _onApiGetAnkiDeckNames() { -        return await this.anki.getDeckNames(); +        return await this._anki.getDeckNames();      }      async _onApiGetAnkiModelNames() { -        return await this.anki.getModelNames(); +        return await this._anki.getModelNames();      }      async _onApiGetAnkiModelFieldNames({modelName}) { -        return await this.anki.getModelFieldNames(modelName); +        return await this._anki.getModelFieldNames(modelName);      }      async _onApiGetDictionaryInfo() { -        return await this.translator.database.getDictionaryInfo(); +        return await this._translator.database.getDictionaryInfo();      }      async _onApiGetDictionaryCounts({dictionaryNames, getTotal}) { -        return await this.translator.database.getDictionaryCounts(dictionaryNames, getTotal); +        return await this._translator.database.getDictionaryCounts(dictionaryNames, getTotal);      }      async _onApiPurgeDatabase() { -        this.translator.clearDatabaseCaches(); -        await this.database.purge(); +        this._translator.clearDatabaseCaches(); +        await this._database.purge();      }      async _onApiGetMedia({targets}) { -        return await this.database.getMedia(targets); +        return await this._database.getMedia(targets);      }      _onApiLog({error, level, context}) { @@ -861,12 +719,12 @@ class Backend {      }      async _onApiImportDictionaryArchive({archiveContent, details}, sender, onProgress) { -        return await this.dictionaryImporter.import(this.database, archiveContent, details, onProgress); +        return await this._dictionaryImporter.import(this._database, archiveContent, details, onProgress);      }      async _onApiDeleteDictionary({dictionaryName}, sender, onProgress) { -        this.translator.clearDatabaseCaches(); -        await this.database.deleteDictionary(dictionaryName, {rate: 1000}, onProgress); +        this._translator.clearDatabaseCaches(); +        await this._database.deleteDictionary(dictionaryName, {rate: 1000}, onProgress);      }      async _onApiModifySettings({targets, source}) { @@ -897,12 +755,246 @@ class Backend {      }      async _onApiSetAllSettings({value, source}) { -        this.options = JsonSchema.getValidValueOrDefault(this.optionsSchema, value); +        this._options = JsonSchema.getValidValueOrDefault(this._optionsSchema, value);          await this._onApiOptionsSave({source});      }      // Command handlers +    async _onCommandSearch(params) { +        const {mode='existingOrNewTab', query} = params || {}; + +        const options = this.getOptions(this._optionsContext); +        const {popupWidth, popupHeight} = options.general; + +        const baseUrl = chrome.runtime.getURL('/bg/search.html'); +        const queryParams = {mode}; +        if (query && query.length > 0) { queryParams.query = query; } +        const queryString = new URLSearchParams(queryParams).toString(); +        const url = `${baseUrl}?${queryString}`; + +        const isTabMatch = (url2) => { +            if (url2 === null || !url2.startsWith(baseUrl)) { return false; } +            const {baseUrl: baseUrl2, queryParams: queryParams2} = parseUrl(url2); +            return baseUrl2 === baseUrl && (queryParams2.mode === mode || (!queryParams2.mode && mode === 'existingOrNewTab')); +        }; + +        const openInTab = async () => { +            const tab = await this._findTab(1000, isTabMatch); +            if (tab !== null) { +                await this._focusTab(tab); +                if (queryParams.query) { +                    await new Promise((resolve) => chrome.tabs.sendMessage( +                        tab.id, +                        {action: 'searchQueryUpdate', params: {text: queryParams.query}}, +                        resolve +                    )); +                } +                return true; +            } +        }; + +        switch (mode) { +            case 'existingOrNewTab': +                try { +                    if (await openInTab()) { return; } +                } catch (e) { +                    // NOP +                } +                chrome.tabs.create({url}); +                return; +            case 'newTab': +                chrome.tabs.create({url}); +                return; +            case 'popup': +                try { +                    // chrome.windows not supported (e.g. on Firefox mobile) +                    if (!isObject(chrome.windows)) { return; } +                    if (await openInTab()) { return; } +                    // if the previous popup is open in an invalid state, close it +                    if (this._popupWindow !== null) { +                        const callback = () => this._checkLastError(chrome.runtime.lastError); +                        chrome.windows.remove(this._popupWindow.id, callback); +                    } +                    // open new popup +                    this._popupWindow = await new Promise((resolve) => chrome.windows.create( +                        {url, width: popupWidth, height: popupHeight, type: 'popup'}, +                        resolve +                    )); +                } catch (e) { +                    // NOP +                } +                return; +        } +    } + +    _onCommandHelp() { +        chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'}); +    } + +    _onCommandOptions(params) { +        const {mode='existingOrNewTab'} = params || {}; +        if (mode === 'existingOrNewTab') { +            chrome.runtime.openOptionsPage(); +        } else if (mode === 'newTab') { +            const manifest = chrome.runtime.getManifest(); +            const url = chrome.runtime.getURL(manifest.options_ui.page); +            chrome.tabs.create({url}); +        } +    } + +    async _onCommandToggle() { +        const source = 'popup'; +        const options = this.getOptions(this._optionsContext); +        options.general.enable = !options.general.enable; +        await this._onApiOptionsSave({source}); +    } + +    // Utilities + +    _sendMessageAllTabs(action, params={}) { +        const callback = () => this._checkLastError(chrome.runtime.lastError); +        chrome.tabs.query({}, (tabs) => { +            for (const tab of tabs) { +                chrome.tabs.sendMessage(tab.id, {action, params}, callback); +            } +        }); +    } + +    _applyOptions(source) { +        const options = this.getOptions(this._optionsContext); +        this._updateBadge(); + +        this._anki.setServer(options.anki.server); +        this._anki.setEnabled(options.anki.enable); + +        if (options.parsing.enableMecabParser) { +            this._mecab.startListener(); +        } else { +            this._mecab.stopListener(); +        } + +        if (options.general.enableClipboardPopups) { +            this._clipboardMonitor.start(); +        } else { +            this._clipboardMonitor.stop(); +        } + +        this._sendMessageAllTabs('optionsUpdated', {source}); +    } + +    _getProfile(optionsContext, useSchema=false) { +        const options = this.getFullOptions(useSchema); +        const profiles = options.profiles; +        if (typeof optionsContext.index === 'number') { +            return profiles[optionsContext.index]; +        } +        const profile = this._getProfileFromContext(options, optionsContext); +        return profile !== null ? profile : options.profiles[options.profileCurrent]; +    } + +    _getProfileFromContext(options, optionsContext) { +        for (const profile of options.profiles) { +            const conditionGroups = profile.conditionGroups; +            if (conditionGroups.length > 0 && this._testConditionGroups(conditionGroups, optionsContext)) { +                return profile; +            } +        } +        return null; +    } + +    _testConditionGroups(conditionGroups, data) { +        if (conditionGroups.length === 0) { return false; } + +        for (const conditionGroup of conditionGroups) { +            const conditions = conditionGroup.conditions; +            if (conditions.length > 0 && this._testConditions(conditions, data)) { +                return true; +            } +        } + +        return false; +    } + +    _testConditions(conditions, data) { +        for (const condition of conditions) { +            if (!conditionsTestValue(profileConditionsDescriptor, condition.type, condition.operator, condition.value, data)) { +                return false; +            } +        } +        return true; +    } + +    _checkLastError() { +        // NOP +    } + +    _runCommand(command, params) { +        const handler = this._commandHandlers.get(command); +        if (typeof handler !== 'function') { return false; } + +        handler(params); +        return true; +    } + +    async _importDictionary(archiveSource, onProgress, details) { +        return await this._dictionaryImporter.import(this._database, archiveSource, onProgress, details); +    } + +    async _textParseScanning(text, options) { +        const results = []; +        while (text.length > 0) { +            const term = []; +            const [definitions, sourceLength] = await this._translator.findTerms( +                'simple', +                text.substring(0, options.scanning.length), +                {}, +                options +            ); +            if (definitions.length > 0 && sourceLength > 0) { +                dictTermsSort(definitions); +                const {expression, reading} = definitions[0]; +                const source = text.substring(0, sourceLength); +                for (const {text: text2, furigana} of jp.distributeFuriganaInflected(expression, reading, source)) { +                    const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode); +                    term.push({text: text2, reading: reading2}); +                } +                text = text.substring(source.length); +            } else { +                const reading = jp.convertReading(text[0], '', options.parsing.readingMode); +                term.push({text: text[0], reading}); +                text = text.substring(1); +            } +            results.push(term); +        } +        return results; +    } + +    async _textParseMecab(text, options) { +        const results = []; +        const rawResults = await this._mecab.parseText(text); +        for (const [mecabName, parsedLines] of Object.entries(rawResults)) { +            const result = []; +            for (const parsedLine of parsedLines) { +                for (const {expression, reading, source} of parsedLine) { +                    const term = []; +                    for (const {text: text2, furigana} of jp.distributeFuriganaInflected( +                        expression.length > 0 ? expression : source, +                        jp.convertKatakanaToHiragana(reading), +                        source +                    )) { +                        const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode); +                        term.push({text: text2, reading: reading2}); +                    } +                    result.push(term); +                } +                result.push([{text: '\n', reading: ''}]); +            } +            results.push([mecabName, result]); +        } +        return results; +    } +      _createActionListenerPort(port, sender, handlers) {          let hasStarted = false;          let messageString = ''; @@ -995,97 +1087,6 @@ class Backend {          }      } -    async _onCommandSearch(params) { -        const {mode='existingOrNewTab', query} = params || {}; - -        const options = this.getOptions(this.optionsContext); -        const {popupWidth, popupHeight} = options.general; - -        const baseUrl = chrome.runtime.getURL('/bg/search.html'); -        const queryParams = {mode}; -        if (query && query.length > 0) { queryParams.query = query; } -        const queryString = new URLSearchParams(queryParams).toString(); -        const url = `${baseUrl}?${queryString}`; - -        const isTabMatch = (url2) => { -            if (url2 === null || !url2.startsWith(baseUrl)) { return false; } -            const {baseUrl: baseUrl2, queryParams: queryParams2} = parseUrl(url2); -            return baseUrl2 === baseUrl && (queryParams2.mode === mode || (!queryParams2.mode && mode === 'existingOrNewTab')); -        }; - -        const openInTab = async () => { -            const tab = await Backend._findTab(1000, isTabMatch); -            if (tab !== null) { -                await Backend._focusTab(tab); -                if (queryParams.query) { -                    await new Promise((resolve) => chrome.tabs.sendMessage( -                        tab.id, -                        {action: 'searchQueryUpdate', params: {text: queryParams.query}}, -                        resolve -                    )); -                } -                return true; -            } -        }; - -        switch (mode) { -            case 'existingOrNewTab': -                try { -                    if (await openInTab()) { return; } -                } catch (e) { -                    // NOP -                } -                chrome.tabs.create({url}); -                return; -            case 'newTab': -                chrome.tabs.create({url}); -                return; -            case 'popup': -                try { -                    // chrome.windows not supported (e.g. on Firefox mobile) -                    if (!isObject(chrome.windows)) { return; } -                    if (await openInTab()) { return; } -                    // if the previous popup is open in an invalid state, close it -                    if (this.popupWindow !== null) { -                        const callback = () => this.checkLastError(chrome.runtime.lastError); -                        chrome.windows.remove(this.popupWindow.id, callback); -                    } -                    // open new popup -                    this.popupWindow = await new Promise((resolve) => chrome.windows.create( -                        {url, width: popupWidth, height: popupHeight, type: 'popup'}, -                        resolve -                    )); -                } catch (e) { -                    // NOP -                } -                return; -        } -    } - -    _onCommandHelp() { -        chrome.tabs.create({url: 'https://foosoft.net/projects/yomichan/'}); -    } - -    _onCommandOptions(params) { -        const {mode='existingOrNewTab'} = params || {}; -        if (mode === 'existingOrNewTab') { -            chrome.runtime.openOptionsPage(); -        } else if (mode === 'newTab') { -            const manifest = chrome.runtime.getManifest(); -            const url = chrome.runtime.getURL(manifest.options_ui.page); -            chrome.tabs.create({url}); -        } -    } - -    async _onCommandToggle() { -        const source = 'popup'; -        const options = this.getOptions(this.optionsContext); -        options.general.enable = !options.general.enable; -        await this._onApiOptionsSave({source}); -    } - -    // Utilities -      _getModifySettingObject(target) {          const scope = target.scope;          switch (scope) { @@ -1235,7 +1236,7 @@ class Backend {      }      _anyOptionsMatches(predicate) { -        for (const {options} of this.options.profiles) { +        for (const {options} of this._options.profiles) {              const value = predicate(options);              if (value) { return value; }          } @@ -1248,10 +1249,10 @@ class Backend {      _getTemplates(options) {          const templates = options.anki.fieldTemplates; -        return typeof templates === 'string' ? templates : this.defaultAnkiFieldTemplates; +        return typeof templates === 'string' ? templates : this._defaultAnkiFieldTemplates;      } -    static _getTabUrl(tab) { +    _getTabUrl(tab) {          return new Promise((resolve) => {              chrome.tabs.sendMessage(tab.id, {action: 'getUrl'}, {frameId: 0}, (response) => {                  let url = null; @@ -1266,7 +1267,7 @@ class Backend {          });      } -    static async _findTab(timeout, checkUrl) { +    async _findTab(timeout, checkUrl) {          // This function works around the need to have the "tabs" permission to access tab.url.          const tabs = await new Promise((resolve) => chrome.tabs.query({}, resolve));          const {promise: matchPromise, resolve: matchPromiseResolve} = deferPromise(); @@ -1279,7 +1280,7 @@ class Backend {          const promises = [];          for (const tab of tabs) { -            const promise = Backend._getTabUrl(tab); +            const promise = this._getTabUrl(tab);              promise.then(checkTabUrl);              promises.push(promise);          } @@ -1295,7 +1296,7 @@ class Backend {          return await Promise.race(racePromises);      } -    static async _focusTab(tab) { +    async _focusTab(tab) {          await new Promise((resolve, reject) => {              chrome.tabs.update(tab.id, {active: true}, () => {                  const e = chrome.runtime.lastError; |