From 5bf805755a33f6f10fd9621f8a2bff7ba1cb7440 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 28 Jun 2020 11:26:43 -0400 Subject: [PATCH] Yomichan object separation (#627) * Move "yomichan" object setup to a separate file * Update script imports * Align message handlers * Rename Yomichan.prepare to Yomichan.ready * Add new prepare function * Improve isExtensionUrl --- .eslintrc.json | 11 +- ext/bg/background.html | 1 + ext/bg/context.html | 1 + ext/bg/js/context-main.js | 2 +- ext/bg/js/search-main.js | 2 +- ext/bg/js/settings/main.js | 2 +- ext/bg/search.html | 1 + ext/bg/settings-popup-preview.html | 1 + ext/bg/settings.html | 1 + ext/fg/float.html | 1 + ext/fg/js/content-script-main.js | 2 +- ext/manifest.json | 1 + ext/mixed/js/core.js | 192 -------------------------- ext/mixed/js/display.js | 2 +- ext/mixed/js/yomichan.js | 208 +++++++++++++++++++++++++++++ test/test-database.js | 2 +- 16 files changed, 231 insertions(+), 199 deletions(-) create mode 100644 ext/mixed/js/yomichan.js diff --git a/.eslintrc.json b/.eslintrc.json index 68b840f8..b8b7bee8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -86,7 +86,6 @@ "files": ["ext/**/*.js"], "excludedFiles": ["ext/mixed/js/core.js"], "globals": { - "yomichan": "readonly", "errorToJson": "readonly", "jsonToError": "readonly", "isObject": "readonly", @@ -105,6 +104,16 @@ "EXTENSION_IS_BROWSER_EDGE": "readonly" } }, + { + "files": ["ext/**/*.js"], + "excludedFiles": [ + "ext/mixed/js/core.js", + "ext/mixed/js/yomichan.js" + ], + "globals": { + "yomichan": "readonly" + } + }, { "files": ["ext/mixed/js/core.js"], "globals": { diff --git a/ext/bg/background.html b/ext/bg/background.html index 55380ae7..11ea002f 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -20,6 +20,7 @@ + diff --git a/ext/bg/context.html b/ext/bg/context.html index 1e7e6155..9ed0544f 100644 --- a/ext/bg/context.html +++ b/ext/bg/context.html @@ -47,6 +47,7 @@ + diff --git a/ext/bg/js/context-main.js b/ext/bg/js/context-main.js index 4a2ea168..8718f583 100644 --- a/ext/bg/js/context-main.js +++ b/ext/bg/js/context-main.js @@ -51,7 +51,7 @@ function setupButtonEvents(selector, command, url) { async function mainInner() { api.forwardLogsToBackend(); - await yomichan.prepare(); + await yomichan.ready(); await api.logIndicatorClear(); diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 13bd8767..bbe6c343 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -23,7 +23,7 @@ (async () => { try { api.forwardLogsToBackend(); - await yomichan.prepare(); + await yomichan.ready(); const displaySearch = new DisplaySearch(); await displaySearch.prepare(); diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js index e22c5e53..48b72735 100644 --- a/ext/bg/js/settings/main.js +++ b/ext/bg/js/settings/main.js @@ -64,7 +64,7 @@ async function setupEnvironmentInfo() { (async () => { api.forwardLogsToBackend(); - await yomichan.prepare(); + await yomichan.ready(); setupEnvironmentInfo(); showExtensionInformation(); diff --git a/ext/bg/search.html b/ext/bg/search.html index 4a28dd88..cfcf1f96 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -71,6 +71,7 @@ + diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index 75bf06c8..c0c8e3b9 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -119,6 +119,7 @@ + diff --git a/ext/bg/settings.html b/ext/bg/settings.html index 51cb14e7..6fa54e23 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -1133,6 +1133,7 @@ + diff --git a/ext/fg/float.html b/ext/fg/float.html index 3e41cde5..735a880a 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -40,6 +40,7 @@ + diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js index 1f3a69e5..6b0706fa 100644 --- a/ext/fg/js/content-script-main.js +++ b/ext/fg/js/content-script-main.js @@ -24,7 +24,7 @@ (async () => { try { api.forwardLogsToBackend(); - await yomichan.prepare(); + await yomichan.ready(); const {frameId} = await api.frameInformationGet(); if (typeof frameId !== 'number') { diff --git a/ext/manifest.json b/ext/manifest.json index c356f3d3..38069aea 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -36,6 +36,7 @@ "matches": ["http://*/*", "https://*/*", "file://*/*"], "js": [ "mixed/js/core.js", + "mixed/js/yomichan.js", "mixed/js/comm.js", "mixed/js/dom.js", "mixed/js/api.js", diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 0fe5ea20..21b7bf5e 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -303,195 +303,3 @@ class EventListenerCollection { this._eventListeners = []; } } - - -/* - * Default message handlers - */ - -const yomichan = (() => { - class Yomichan extends EventDispatcher { - constructor() { - super(); - - this._isBackendPreparedPromise = this.getTemporaryListenerResult( - chrome.runtime.onMessage, - ({action}, {resolve}) => { - if (action === 'backendPrepared') { - resolve(); - } - } - ); - - this._messageHandlers = new Map([ - ['getUrl', this._onMessageGetUrl.bind(this)], - ['optionsUpdated', this._onMessageOptionsUpdated.bind(this)], - ['zoomChanged', this._onMessageZoomChanged.bind(this)] - ]); - - chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); - } - - // Public - - prepare() { - chrome.runtime.sendMessage({action: 'yomichanCoreReady'}); - return this._isBackendPreparedPromise; - } - - generateId(length) { - const array = new Uint8Array(length); - crypto.getRandomValues(array); - let id = ''; - for (const value of array) { - id += value.toString(16).padStart(2, '0'); - } - return id; - } - - triggerOrphaned(error) { - this.trigger('orphaned', {error}); - } - - isExtensionUrl(url) { - try { - const urlBase = chrome.runtime.getURL('/'); - return url.substring(0, urlBase.length) === urlBase; - } catch (e) { - return false; - } - } - - getTemporaryListenerResult(eventHandler, userCallback, timeout=null) { - if (!( - typeof eventHandler.addListener === 'function' && - typeof eventHandler.removeListener === 'function' - )) { - throw new Error('Event handler type not supported'); - } - - return new Promise((resolve, reject) => { - const runtimeMessageCallback = ({action, params}, sender, sendResponse) => { - let timeoutId = null; - if (timeout !== null) { - timeoutId = setTimeout(() => { - timeoutId = null; - eventHandler.removeListener(runtimeMessageCallback); - reject(new Error(`Listener timed out in ${timeout} ms`)); - }, timeout); - } - - const cleanupResolve = (value) => { - if (timeoutId !== null) { - clearTimeout(timeoutId); - timeoutId = null; - } - eventHandler.removeListener(runtimeMessageCallback); - sendResponse(); - resolve(value); - }; - - userCallback({action, params}, {resolve: cleanupResolve, sender}); - }; - - eventHandler.addListener(runtimeMessageCallback); - }); - } - - logWarning(error) { - this.log(error, 'warn'); - } - - logError(error) { - this.log(error, 'error'); - } - - log(error, level, context=null) { - if (!isObject(context)) { - context = this._getLogContext(); - } - - let errorString; - try { - errorString = error.toString(); - if (/^\[object \w+\]$/.test(errorString)) { - errorString = JSON.stringify(error); - } - } catch (e) { - errorString = `${error}`; - } - - let errorStack; - try { - errorStack = (typeof error.stack === 'string' ? error.stack.trimRight() : ''); - } catch (e) { - errorStack = ''; - } - - let errorData; - try { - errorData = error.data; - } catch (e) { - // NOP - } - - if (errorStack.startsWith(errorString)) { - errorString = errorStack; - } else if (errorStack.length > 0) { - errorString += `\n${errorStack}`; - } - - const manifest = chrome.runtime.getManifest(); - let message = `${manifest.name} v${manifest.version} has encountered a problem.`; - message += `\nOriginating URL: ${context.url}\n`; - message += errorString; - if (typeof errorData !== 'undefined') { - message += `\nData: ${JSON.stringify(errorData, null, 4)}`; - } - message += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; - - switch (level) { - case 'info': console.info(message); break; - case 'debug': console.debug(message); break; - case 'warn': console.warn(message); break; - case 'error': console.error(message); break; - default: console.log(message); break; - } - - this.trigger('log', {error, level, context}); - } - - // Private - - _getUrl() { - return (typeof window === 'object' && window !== null ? window.location.href : ''); - } - - _getLogContext() { - return {url: this._getUrl()}; - } - - _onMessage({action, params}, sender, callback) { - const handler = this._messageHandlers.get(action); - if (typeof handler !== 'function') { return false; } - - const result = handler(params, sender); - callback(result); - return false; - } - - _onMessageGetUrl() { - return {url: this._getUrl()}; - } - - _onMessageOptionsUpdated({source}) { - this.trigger('optionsUpdated', {source}); - } - - _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) { - this.trigger('zoomChanged', {oldZoomFactor, newZoomFactor}); - } - } - - return new Yomichan(); -})(); diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index 1d699706..e7439796 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -167,7 +167,7 @@ class Display { } async prepare() { - await yomichan.prepare(); + await yomichan.ready(); await this.displayGenerator.prepare(); } diff --git a/ext/mixed/js/yomichan.js b/ext/mixed/js/yomichan.js new file mode 100644 index 00000000..5fa504ef --- /dev/null +++ b/ext/mixed/js/yomichan.js @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2020 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const yomichan = (() => { + class Yomichan extends EventDispatcher { + constructor() { + super(); + + const {promise, resolve} = deferPromise(); + this._isBackendPreparedPromise = promise; + this._isBackendPreparedPromiseResolve = resolve; + + this._messageHandlers = new Map([ + ['backendPrepared', this._onMessageBackendPrepared.bind(this)], + ['getUrl', this._onMessageGetUrl.bind(this)], + ['optionsUpdated', this._onMessageOptionsUpdated.bind(this)], + ['zoomChanged', this._onMessageZoomChanged.bind(this)] + ]); + } + + // Public + + prepare() { + chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); + } + + ready() { + chrome.runtime.sendMessage({action: 'yomichanCoreReady'}); + return this._isBackendPreparedPromise; + } + + generateId(length) { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + let id = ''; + for (const value of array) { + id += value.toString(16).padStart(2, '0'); + } + return id; + } + + triggerOrphaned(error) { + this.trigger('orphaned', {error}); + } + + isExtensionUrl(url) { + try { + return url.startsWith(chrome.runtime.getURL('/')); + } catch (e) { + return false; + } + } + + getTemporaryListenerResult(eventHandler, userCallback, timeout=null) { + if (!( + typeof eventHandler.addListener === 'function' && + typeof eventHandler.removeListener === 'function' + )) { + throw new Error('Event handler type not supported'); + } + + return new Promise((resolve, reject) => { + const runtimeMessageCallback = ({action, params}, sender, sendResponse) => { + let timeoutId = null; + if (timeout !== null) { + timeoutId = setTimeout(() => { + timeoutId = null; + eventHandler.removeListener(runtimeMessageCallback); + reject(new Error(`Listener timed out in ${timeout} ms`)); + }, timeout); + } + + const cleanupResolve = (value) => { + if (timeoutId !== null) { + clearTimeout(timeoutId); + timeoutId = null; + } + eventHandler.removeListener(runtimeMessageCallback); + sendResponse(); + resolve(value); + }; + + userCallback({action, params}, {resolve: cleanupResolve, sender}); + }; + + eventHandler.addListener(runtimeMessageCallback); + }); + } + + logWarning(error) { + this.log(error, 'warn'); + } + + logError(error) { + this.log(error, 'error'); + } + + log(error, level, context=null) { + if (!isObject(context)) { + context = this._getLogContext(); + } + + let errorString; + try { + errorString = error.toString(); + if (/^\[object \w+\]$/.test(errorString)) { + errorString = JSON.stringify(error); + } + } catch (e) { + errorString = `${error}`; + } + + let errorStack; + try { + errorStack = (typeof error.stack === 'string' ? error.stack.trimRight() : ''); + } catch (e) { + errorStack = ''; + } + + let errorData; + try { + errorData = error.data; + } catch (e) { + // NOP + } + + if (errorStack.startsWith(errorString)) { + errorString = errorStack; + } else if (errorStack.length > 0) { + errorString += `\n${errorStack}`; + } + + const manifest = chrome.runtime.getManifest(); + let message = `${manifest.name} v${manifest.version} has encountered a problem.`; + message += `\nOriginating URL: ${context.url}\n`; + message += errorString; + if (typeof errorData !== 'undefined') { + message += `\nData: ${JSON.stringify(errorData, null, 4)}`; + } + message += '\n\nIssues can be reported at https://github.com/FooSoft/yomichan/issues'; + + switch (level) { + case 'info': console.info(message); break; + case 'debug': console.debug(message); break; + case 'warn': console.warn(message); break; + case 'error': console.error(message); break; + default: console.log(message); break; + } + + this.trigger('log', {error, level, context}); + } + + // Private + + _getUrl() { + return (typeof window === 'object' && window !== null ? window.location.href : ''); + } + + _getLogContext() { + return {url: this._getUrl()}; + } + + _onMessage({action, params}, sender, callback) { + const handler = this._messageHandlers.get(action); + if (typeof handler !== 'function') { return false; } + + const result = handler(params, sender); + callback(result); + return false; + } + + _onMessageBackendPrepared() { + if (this._isBackendPreparedPromiseResolve === null) { return; } + this._isBackendPreparedPromiseResolve(); + this._isBackendPreparedPromiseResolve = null; + } + + _onMessageGetUrl() { + return {url: this._getUrl()}; + } + + _onMessageOptionsUpdated({source}) { + this.trigger('optionsUpdated', {source}); + } + + _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) { + this.trigger('zoomChanged', {oldZoomFactor, newZoomFactor}); + } + } + + return new Yomichan(); +})(); + +yomichan.prepare(); diff --git a/test/test-database.js b/test/test-database.js index 03b2bd3b..5291c138 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -109,9 +109,9 @@ const vm = new VM({ vm.context.window = vm.context; vm.execute([ + 'mixed/js/core.js', 'bg/js/json-schema.js', 'bg/js/dictionary.js', - 'mixed/js/core.js', 'bg/js/media-utility.js', 'bg/js/request.js', 'bg/js/dictionary-importer.js',