aboutsummaryrefslogtreecommitdiff
path: root/ext/js
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2024-01-19 23:52:48 -0500
committerGitHub <noreply@github.com>2024-01-20 04:52:48 +0000
commit39265a43d969e1201cb5267789967b57835059b1 (patch)
treee6ff82234037b16782b85d77901b29bbe12137fd /ext/js
parentffddc74f3d05061fc1e84d149ce678d116cedc4c (diff)
Separate core classes into separate files (#545)
Diffstat (limited to 'ext/js')
-rw-r--r--ext/js/accessibility/accessibility-controller.js2
-rw-r--r--ext/js/app/content-script-main.js2
-rw-r--r--ext/js/app/frontend.js4
-rw-r--r--ext/js/app/popup-factory.js2
-rw-r--r--ext/js/app/popup-proxy.js3
-rw-r--r--ext/js/app/popup-window.js2
-rw-r--r--ext/js/app/popup.js5
-rw-r--r--ext/js/background/backend.js3
-rw-r--r--ext/js/background/offscreen-proxy.js2
-rw-r--r--ext/js/comm/clipboard-monitor.js2
-rw-r--r--ext/js/comm/cross-frame-api.js4
-rw-r--r--ext/js/comm/frame-ancestry-handler.js2
-rw-r--r--ext/js/comm/frame-client.js2
-rw-r--r--ext/js/comm/frame-endpoint.js3
-rw-r--r--ext/js/comm/mecab.js2
-rw-r--r--ext/js/core.js737
-rw-r--r--ext/js/core/dynamic-property.js131
-rw-r--r--ext/js/core/event-dispatcher.js105
-rw-r--r--ext/js/core/event-listener-collection.js97
-rw-r--r--ext/js/core/logger.js157
-rw-r--r--ext/js/core/utilities.js319
-rw-r--r--ext/js/data/anki-note-builder.js2
-rw-r--r--ext/js/data/anki-util.js2
-rw-r--r--ext/js/data/json-schema.js2
-rw-r--r--ext/js/data/options-util.js2
-rw-r--r--ext/js/dictionary/dictionary-database.js3
-rw-r--r--ext/js/dictionary/dictionary-importer-media-loader.js2
-rw-r--r--ext/js/dictionary/dictionary-importer.js2
-rw-r--r--ext/js/dictionary/dictionary-worker-main.js2
-rw-r--r--ext/js/dictionary/dictionary-worker-media-loader.js2
-rw-r--r--ext/js/display/display-anki.js3
-rw-r--r--ext/js/display/display-audio.js2
-rw-r--r--ext/js/display/display-content-manager.js2
-rw-r--r--ext/js/display/display-generator.js2
-rw-r--r--ext/js/display/display-history.js3
-rw-r--r--ext/js/display/display-notification.js2
-rw-r--r--ext/js/display/display-profile-selection.js3
-rw-r--r--ext/js/display/display-resizer.js2
-rw-r--r--ext/js/display/display.js6
-rw-r--r--ext/js/display/element-overflow-controller.js2
-rw-r--r--ext/js/display/option-toggle-hotkey-handler.js2
-rw-r--r--ext/js/display/popup-main.js2
-rw-r--r--ext/js/display/query-parser.js3
-rw-r--r--ext/js/display/search-display-controller.js2
-rw-r--r--ext/js/display/search-main.js2
-rw-r--r--ext/js/display/search-persistent-state-controller.js2
-rw-r--r--ext/js/dom/document-util.js2
-rw-r--r--ext/js/dom/panel-element.js2
-rw-r--r--ext/js/dom/popup-menu.js3
-rw-r--r--ext/js/general/task-accumulator.js2
-rw-r--r--ext/js/input/hotkey-handler.js3
-rw-r--r--ext/js/input/hotkey-help-controller.js2
-rw-r--r--ext/js/language/text-scanner.js5
-rw-r--r--ext/js/media/audio-system.js2
-rw-r--r--ext/js/pages/info-main.js3
-rw-r--r--ext/js/pages/permissions-main.js3
-rw-r--r--ext/js/pages/settings/anki-controller.js3
-rw-r--r--ext/js/pages/settings/audio-controller.js3
-rw-r--r--ext/js/pages/settings/backup-controller.js3
-rw-r--r--ext/js/pages/settings/collapsible-dictionary-controller.js2
-rw-r--r--ext/js/pages/settings/dictionary-controller.js3
-rw-r--r--ext/js/pages/settings/dictionary-import-controller.js2
-rw-r--r--ext/js/pages/settings/extension-keyboard-shortcuts-controller.js3
-rw-r--r--ext/js/pages/settings/keyboard-mouse-input-field.js3
-rw-r--r--ext/js/pages/settings/keyboard-shortcuts-controller.js2
-rw-r--r--ext/js/pages/settings/permissions-origin-controller.js2
-rw-r--r--ext/js/pages/settings/persistent-storage-controller.js2
-rw-r--r--ext/js/pages/settings/popup-preview-frame-main.js2
-rw-r--r--ext/js/pages/settings/profile-conditions-ui.js3
-rw-r--r--ext/js/pages/settings/profile-controller.js3
-rw-r--r--ext/js/pages/settings/recommended-permissions-controller.js2
-rw-r--r--ext/js/pages/settings/scan-inputs-controller.js2
-rw-r--r--ext/js/pages/settings/secondary-search-dictionary-controller.js2
-rw-r--r--ext/js/pages/settings/sentence-termination-characters-controller.js2
-rw-r--r--ext/js/pages/settings/settings-controller.js4
-rw-r--r--ext/js/pages/settings/settings-main.js2
-rw-r--r--ext/js/pages/settings/translation-text-replacements-controller.js2
-rw-r--r--ext/js/pages/welcome-main.js2
-rw-r--r--ext/js/templates/template-renderer-proxy.js2
-rw-r--r--ext/js/yomitan.js4
80 files changed, 921 insertions, 811 deletions
diff --git a/ext/js/accessibility/accessibility-controller.js b/ext/js/accessibility/accessibility-controller.js
index 3cc400e4..b2785893 100644
--- a/ext/js/accessibility/accessibility-controller.js
+++ b/ext/js/accessibility/accessibility-controller.js
@@ -17,7 +17,7 @@
*/
import {isContentScriptRegistered, registerContentScript, unregisterContentScript} from '../background/script-manager.js';
-import {log} from '../core.js';
+import {log} from '../core/logger.js';
/**
* This class controls the registration of accessibility handlers.
diff --git a/ext/js/app/content-script-main.js b/ext/js/app/content-script-main.js
index e640dc3c..c0bea73c 100644
--- a/ext/js/app/content-script-main.js
+++ b/ext/js/app/content-script-main.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log} from '../core.js';
+import {log} from '../core/logger.js';
import {HotkeyHandler} from '../input/hotkey-handler.js';
import {yomitan} from '../yomitan.js';
import {Frontend} from './frontend.js';
diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js
index 8ac3acc3..13d2d9d8 100644
--- a/ext/js/app/frontend.js
+++ b/ext/js/app/frontend.js
@@ -16,8 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection, log, promiseAnimationFrame} from '../core.js';
import {createApiMap, invokeApiMapHandler} from '../core/api-map.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
+import {log} from '../core/logger.js';
+import {promiseAnimationFrame} from '../core/utilities.js';
import {DocumentUtil} from '../dom/document-util.js';
import {TextSourceElement} from '../dom/text-source-element.js';
import {TextSourceRange} from '../dom/text-source-range.js';
diff --git a/ext/js/app/popup-factory.js b/ext/js/app/popup-factory.js
index 14fc4617..f9eec913 100644
--- a/ext/js/app/popup-factory.js
+++ b/ext/js/app/popup-factory.js
@@ -17,7 +17,7 @@
*/
import {FrameOffsetForwarder} from '../comm/frame-offset-forwarder.js';
-import {generateId} from '../core.js';
+import {generateId} from '../core/utilities.js';
import {yomitan} from '../yomitan.js';
import {PopupProxy} from './popup-proxy.js';
import {PopupWindow} from './popup-window.js';
diff --git a/ext/js/app/popup-proxy.js b/ext/js/app/popup-proxy.js
index 66c91994..fa4a448b 100644
--- a/ext/js/app/popup-proxy.js
+++ b/ext/js/app/popup-proxy.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, log} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {log} from '../core/logger.js';
import {yomitan} from '../yomitan.js';
/**
diff --git a/ext/js/app/popup-window.js b/ext/js/app/popup-window.js
index ff8a1273..60d99612 100644
--- a/ext/js/app/popup-window.js
+++ b/ext/js/app/popup-window.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
import {yomitan} from '../yomitan.js';
/**
diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js
index d83618d3..0a84f3f7 100644
--- a/ext/js/app/popup.js
+++ b/ext/js/app/popup.js
@@ -17,8 +17,11 @@
*/
import {FrameClient} from '../comm/frame-client.js';
-import {DynamicProperty, EventDispatcher, EventListenerCollection, deepEqual} from '../core.js';
+import {DynamicProperty} from '../core/dynamic-property.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {ExtensionError} from '../core/extension-error.js';
+import {deepEqual} from '../core/utilities.js';
import {DocumentUtil} from '../dom/document-util.js';
import {loadStyle} from '../dom/style-util.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js
index f2f2fda9..38d82496 100644
--- a/ext/js/background/backend.js
+++ b/ext/js/background/backend.js
@@ -22,10 +22,11 @@ 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, isObject, log, promiseTimeout} from '../core.js';
import {createApiMap, invokeApiMapHandler} from '../core/api-map.js';
import {ExtensionError} from '../core/extension-error.js';
import {readResponseJson} from '../core/json.js';
+import {log} from '../core/logger.js';
+import {clone, deferPromise, isObject, promiseTimeout} from '../core/utilities.js';
import {AnkiUtil} from '../data/anki-util.js';
import {OptionsUtil} from '../data/options-util.js';
import {PermissionsUtil} from '../data/permissions-util.js';
diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js
index abd3fa37..77f5448a 100644
--- a/ext/js/background/offscreen-proxy.js
+++ b/ext/js/background/offscreen-proxy.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {isObject} from '../core.js';
+import {isObject} from '../core/utilities.js';
import {ExtensionError} from '../core/extension-error.js';
import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js';
diff --git a/ext/js/comm/clipboard-monitor.js b/ext/js/comm/clipboard-monitor.js
index c4381910..a1ea3362 100644
--- a/ext/js/comm/clipboard-monitor.js
+++ b/ext/js/comm/clipboard-monitor.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
/**
* @augments EventDispatcher<import('clipboard-monitor').Events>
diff --git a/ext/js/comm/cross-frame-api.js b/ext/js/comm/cross-frame-api.js
index ab21967a..fca7c84d 100644
--- a/ext/js/comm/cross-frame-api.js
+++ b/ext/js/comm/cross-frame-api.js
@@ -16,10 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, EventListenerCollection, log} from '../core.js';
import {extendApiMap, invokeApiMapHandler} from '../core/api-map.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {ExtensionError} from '../core/extension-error.js';
import {parseJson} from '../core/json.js';
+import {log} from '../core/logger.js';
import {yomitan} from '../yomitan.js';
/**
diff --git a/ext/js/comm/frame-ancestry-handler.js b/ext/js/comm/frame-ancestry-handler.js
index a9e49a6e..31739654 100644
--- a/ext/js/comm/frame-ancestry-handler.js
+++ b/ext/js/comm/frame-ancestry-handler.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {generateId} from '../core.js';
+import {generateId} from '../core/utilities.js';
import {yomitan} from '../yomitan.js';
/**
diff --git a/ext/js/comm/frame-client.js b/ext/js/comm/frame-client.js
index aadb4f08..62db1edd 100644
--- a/ext/js/comm/frame-client.js
+++ b/ext/js/comm/frame-client.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {deferPromise, generateId, isObject} from '../core.js';
+import {deferPromise, generateId, isObject} from '../core/utilities.js';
export class FrameClient {
constructor() {
diff --git a/ext/js/comm/frame-endpoint.js b/ext/js/comm/frame-endpoint.js
index bf5efdd4..0008417d 100644
--- a/ext/js/comm/frame-endpoint.js
+++ b/ext/js/comm/frame-endpoint.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection, generateId} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
+import {generateId} from '../core/utilities.js';
import {yomitan} from '../yomitan.js';
export class FrameEndpoint {
diff --git a/ext/js/comm/mecab.js b/ext/js/comm/mecab.js
index 57d97e98..1ff3f066 100644
--- a/ext/js/comm/mecab.js
+++ b/ext/js/comm/mecab.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {toError} from '../core/to-error.js';
/**
diff --git a/ext/js/core.js b/ext/js/core.js
deleted file mode 100644
index 1c2ff06b..00000000
--- a/ext/js/core.js
+++ /dev/null
@@ -1,737 +0,0 @@
-/*
- * Copyright (C) 2023-2024 Yomitan Authors
- * Copyright (C) 2019-2022 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 <https://www.gnu.org/licenses/>.
- */
-
-import {ExtensionError} from './core/extension-error.js';
-
-/**
- * Checks whether a given value is a non-array object.
- * @param {unknown} value The value to check.
- * @returns {boolean} `true` if the value is an object and not an array, `false` otherwise.
- */
-export function isObject(value) {
- return typeof value === 'object' && value !== null && !Array.isArray(value);
-}
-
-/**
- * Converts any string into a form that can be passed into the RegExp constructor.
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
- * @param {string} string The string to convert to a valid regular expression.
- * @returns {string} The escaped string.
- */
-export function escapeRegExp(string) {
- return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
-}
-
-/**
- * Reverses a string.
- * @param {string} string The string to reverse.
- * @returns {string} The returned string, which retains proper UTF-16 surrogate pair order.
- */
-export function stringReverse(string) {
- return [...string].reverse().join('');
-}
-
-/**
- * Creates a deep clone of an object or value. This is similar to `parseJson(JSON.stringify(value))`.
- * @template [T=unknown]
- * @param {T} value The value to clone.
- * @returns {T} A new clone of the value.
- * @throws An error if the value is circular and cannot be cloned.
- */
-export function clone(value) {
- if (value === null) { return /** @type {T} */ (null); }
- switch (typeof value) {
- case 'boolean':
- case 'number':
- case 'string':
- case 'bigint':
- case 'symbol':
- case 'undefined':
- return value;
- default:
- return cloneInternal(value, new Set());
- }
-}
-
-/**
- * @template [T=unknown]
- * @param {T} value
- * @param {Set<unknown>} visited
- * @returns {T}
- * @throws {Error}
- */
-function cloneInternal(value, visited) {
- if (value === null) { return /** @type {T} */ (null); }
- switch (typeof value) {
- case 'boolean':
- case 'number':
- case 'string':
- case 'bigint':
- case 'symbol':
- case 'undefined':
- return value;
- case 'object':
- return /** @type {T} */ (
- Array.isArray(value) ?
- cloneArray(value, visited) :
- cloneObject(/** @type {import('core').SerializableObject} */ (value), visited)
- );
- default:
- throw new Error(`Cannot clone object of type ${typeof value}`);
- }
-}
-
-/**
- * @param {unknown[]} value
- * @param {Set<unknown>} visited
- * @returns {unknown[]}
- * @throws {Error}
- */
-function cloneArray(value, visited) {
- if (visited.has(value)) { throw new Error('Circular'); }
- try {
- visited.add(value);
- const result = [];
- for (const item of value) {
- result.push(cloneInternal(item, visited));
- }
- return result;
- } finally {
- visited.delete(value);
- }
-}
-
-/**
- * @param {import('core').SerializableObject} value
- * @param {Set<unknown>} visited
- * @returns {import('core').SerializableObject}
- * @throws {Error}
- */
-function cloneObject(value, visited) {
- if (visited.has(value)) { throw new Error('Circular'); }
- try {
- visited.add(value);
- /** @type {import('core').SerializableObject} */
- const result = {};
- for (const key in value) {
- if (Object.prototype.hasOwnProperty.call(value, key)) {
- result[key] = cloneInternal(value[key], visited);
- }
- }
- return result;
- } finally {
- visited.delete(value);
- }
-}
-
-/**
- * Checks if an object or value is deeply equal to another object or value.
- * @param {unknown} value1 The first value to check.
- * @param {unknown} value2 The second value to check.
- * @returns {boolean} `true` if the values are the same object, or deeply equal without cycles. `false` otherwise.
- */
-export function deepEqual(value1, value2) {
- if (value1 === value2) { return true; }
-
- const type = typeof value1;
- if (typeof value2 !== type) { return false; }
-
- switch (type) {
- case 'object':
- case 'function':
- return deepEqualInternal(value1, value2, new Set());
- default:
- return false;
- }
-}
-
-/**
- * @param {unknown} value1
- * @param {unknown} value2
- * @param {Set<unknown>} visited1
- * @returns {boolean}
- */
-function deepEqualInternal(value1, value2, visited1) {
- if (value1 === value2) { return true; }
-
- const type = typeof value1;
- if (typeof value2 !== type) { return false; }
-
- switch (type) {
- case 'object':
- case 'function':
- {
- if (value1 === null || value2 === null) { return false; }
- const array = Array.isArray(value1);
- if (array !== Array.isArray(value2)) { return false; }
- if (visited1.has(value1)) { return false; }
- visited1.add(value1);
- return (
- array ?
- areArraysEqual(/** @type {unknown[]} */ (value1), /** @type {unknown[]} */ (value2), visited1) :
- areObjectsEqual(/** @type {import('core').UnknownObject} */ (value1), /** @type {import('core').UnknownObject} */ (value2), visited1)
- );
- }
- default:
- return false;
- }
-}
-
-/**
- * @param {import('core').UnknownObject} value1
- * @param {import('core').UnknownObject} value2
- * @param {Set<unknown>} visited1
- * @returns {boolean}
- */
-function areObjectsEqual(value1, value2, visited1) {
- const keys1 = Object.keys(value1);
- const keys2 = Object.keys(value2);
- if (keys1.length !== keys2.length) { return false; }
-
- const keys1Set = new Set(keys1);
- for (const key of keys2) {
- if (!keys1Set.has(key) || !deepEqualInternal(value1[key], value2[key], visited1)) { return false; }
- }
-
- return true;
-}
-
-/**
- * @param {unknown[]} value1
- * @param {unknown[]} value2
- * @param {Set<unknown>} visited1
- * @returns {boolean}
- */
-function areArraysEqual(value1, value2, visited1) {
- const length = value1.length;
- if (length !== value2.length) { return false; }
-
- for (let i = 0; i < length; ++i) {
- if (!deepEqualInternal(value1[i], value2[i], visited1)) { return false; }
- }
-
- return true;
-}
-
-/**
- * Creates a new base-16 (lower case) string of a sequence of random bytes of the given length.
- * @param {number} length The number of bytes the string represents. The returned string's length will be twice as long.
- * @returns {string} A string of random characters.
- */
-export function 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;
-}
-
-/**
- * Creates an unresolved promise that can be resolved later, outside the promise's executor function.
- * @template [T=unknown]
- * @returns {import('core').DeferredPromiseDetails<T>} An object `{promise, resolve, reject}`, containing the promise and the resolve/reject functions.
- */
-export function deferPromise() {
- /** @type {((value: T) => void)|undefined} */
- let resolve;
- /** @type {((reason?: import('core').RejectionReason) => void)|undefined} */
- let reject;
- const promise = new Promise((resolve2, reject2) => {
- resolve = resolve2;
- reject = reject2;
- });
- return {
- promise,
- resolve: /** @type {(value: T) => void} */ (resolve),
- reject: /** @type {(reason?: import('core').RejectionReason) => void} */ (reject)
- };
-}
-
-/**
- * Creates a promise that is resolved after a set delay.
- * @param {number} delay How many milliseconds until the promise should be resolved. If 0, the promise is immediately resolved.
- * @returns {Promise<void>} A promise with two additional properties: `resolve` and `reject`, which can be used to complete the promise early.
- */
-export function promiseTimeout(delay) {
- return delay <= 0 ? Promise.resolve() : new Promise((resolve) => { setTimeout(resolve, delay); });
-}
-
-/**
- * Creates a promise that will resolve after the next animation frame, using `requestAnimationFrame`.
- * @param {number} [timeout] A maximum duration (in milliseconds) to wait until the promise resolves. If null or omitted, no timeout is used.
- * @returns {Promise<{time: number, timeout: boolean}>} A promise that is resolved with `{time, timeout}`, where `time` is the timestamp from `requestAnimationFrame`,
- * and `timeout` is a boolean indicating whether the cause was a timeout or not.
- * @throws The promise throws an error if animation is not supported in this context, such as in a service worker.
- */
-export function promiseAnimationFrame(timeout) {
- return new Promise((resolve, reject) => {
- if (typeof cancelAnimationFrame !== 'function' || typeof requestAnimationFrame !== 'function') {
- reject(new Error('Animation not supported in this context'));
- return;
- }
-
- /** @type {?import('core').Timeout} */
- let timer = null;
- /** @type {?number} */
- let frameRequest = null;
- /**
- * @param {number} time
- */
- const onFrame = (time) => {
- frameRequest = null;
- if (timer !== null) {
- clearTimeout(timer);
- timer = null;
- }
- resolve({time, timeout: false});
- };
- const onTimeout = () => {
- timer = null;
- if (frameRequest !== null) {
- // eslint-disable-next-line no-undef
- cancelAnimationFrame(frameRequest);
- frameRequest = null;
- }
- resolve({time: performance.now(), timeout: true});
- };
-
- // eslint-disable-next-line no-undef
- frameRequest = requestAnimationFrame(onFrame);
- if (typeof timeout === 'number') {
- timer = setTimeout(onTimeout, timeout);
- }
- });
-}
-
-/**
- * The following typedef is required because the JSDoc `implements` tag doesn't work with `import()`.
- * https://github.com/microsoft/TypeScript/issues/49905
- * @typedef {import('core').EventDispatcherOffGeneric} EventDispatcherOffGeneric
- */
-
-/**
- * Base class controls basic event dispatching.
- * @template {import('core').EventSurface} TSurface
- * @implements {EventDispatcherOffGeneric}
- */
-export class EventDispatcher {
- /**
- * Creates a new instance.
- */
- constructor() {
- /** @type {Map<import('core').EventNames<TSurface>, import('core').EventHandlerAny[]>} */
- this._eventMap = new Map();
- }
-
- /**
- * Triggers an event with the given name and specified argument.
- * @template {import('core').EventNames<TSurface>} TName
- * @param {TName} eventName The string representing the event's name.
- * @param {import('core').EventArgument<TSurface, TName>} details The argument passed to the callback functions.
- * @returns {boolean} `true` if any callbacks were registered, `false` otherwise.
- */
- trigger(eventName, details) {
- const callbacks = this._eventMap.get(eventName);
- if (typeof callbacks === 'undefined') { return false; }
-
- for (const callback of callbacks) {
- callback(details);
- }
- return true;
- }
-
- /**
- * Adds a single event listener to a specific event.
- * @template {import('core').EventNames<TSurface>} TName
- * @param {TName} eventName The string representing the event's name.
- * @param {import('core').EventHandler<TSurface, TName>} callback The event listener callback to add.
- */
- on(eventName, callback) {
- let callbacks = this._eventMap.get(eventName);
- if (typeof callbacks === 'undefined') {
- callbacks = [];
- this._eventMap.set(eventName, callbacks);
- }
- callbacks.push(callback);
- }
-
- /**
- * Removes a single event listener from a specific event.
- * @template {import('core').EventNames<TSurface>} TName
- * @param {TName} eventName The string representing the event's name.
- * @param {import('core').EventHandler<TSurface, TName>} callback The event listener callback to add.
- * @returns {boolean} `true` if the callback was removed, `false` otherwise.
- */
- off(eventName, callback) {
- const callbacks = this._eventMap.get(eventName);
- if (typeof callbacks === 'undefined') { return false; }
-
- const ii = callbacks.length;
- for (let i = 0; i < ii; ++i) {
- if (callbacks[i] === callback) {
- callbacks.splice(i, 1);
- if (callbacks.length === 0) {
- this._eventMap.delete(eventName);
- }
- return true;
- }
- }
- return false;
- }
-
- /**
- * Checks if an event has any listeners.
- * @template {import('core').EventNames<TSurface>} TName
- * @param {TName} eventName The string representing the event's name.
- * @returns {boolean} `true` if the event has listeners, `false` otherwise.
- */
- hasListeners(eventName) {
- const callbacks = this._eventMap.get(eventName);
- return (typeof callbacks !== 'undefined' && callbacks.length > 0);
- }
-}
-
-/**
- * Class which stores event listeners added to various objects, making it easy to remove them in bulk.
- */
-export class EventListenerCollection {
- /**
- * Creates a new instance.
- */
- constructor() {
- /** @type {import('event-listener-collection').EventListenerDetails[]} */
- this._eventListeners = [];
- }
-
- /**
- * Returns the number of event listeners that are currently in the object.
- * @type {number}
- */
- get size() {
- return this._eventListeners.length;
- }
-
- /**
- * Adds an event listener using `object.addEventListener`. The listener will later be removed using `object.removeEventListener`.
- * @param {import('event-listener-collection').EventTarget} target The object to add the event listener to.
- * @param {string} type The name of the event.
- * @param {EventListener | EventListenerObject | import('event-listener-collection').EventListenerFunction} listener The callback listener.
- * @param {AddEventListenerOptions | boolean} [options] Options for the event.
- */
- addEventListener(target, type, listener, options) {
- target.addEventListener(type, listener, options);
- this._eventListeners.push({type: 'removeEventListener', target, eventName: type, listener, options});
- }
-
- /**
- * Adds an event listener using `object.addListener`. The listener will later be removed using `object.removeListener`.
- * @template {import('event-listener-collection').EventListenerFunction} TCallback
- * @template [TArgs=unknown]
- * @param {import('event-listener-collection').ExtensionEvent<TCallback, TArgs>} target The object to add the event listener to.
- * @param {TCallback} callback The callback.
- * @param {TArgs[]} args The extra argument array passed to the `addListener`/`removeListener` function.
- */
- addListener(target, callback, ...args) {
- target.addListener(callback, ...args);
- this._eventListeners.push({type: 'removeListener', target, callback, args});
- }
-
- /**
- * Adds an event listener using `object.on`. The listener will later be removed using `object.off`.
- * @template {import('core').EventSurface} TSurface
- * @template {import('core').EventNames<TSurface>} TName
- * @param {EventDispatcher<TSurface>} target The object to add the event listener to.
- * @param {TName} eventName The string representing the event's name.
- * @param {import('core').EventHandler<TSurface, TName>} callback The event listener callback to add.
- */
- on(target, eventName, callback) {
- target.on(eventName, callback);
- this._eventListeners.push({type: 'off', eventName, target, callback});
- }
-
- /**
- * Removes all event listeners added to objects for this instance and clears the internal list of event listeners.
- */
- removeAllEventListeners() {
- if (this._eventListeners.length === 0) { return; }
- for (const item of this._eventListeners) {
- switch (item.type) {
- case 'removeEventListener':
- item.target.removeEventListener(item.eventName, item.listener, item.options);
- break;
- case 'removeListener':
- item.target.removeListener(item.callback, ...item.args);
- break;
- case 'off':
- item.target.off(item.eventName, item.callback);
- break;
- }
- }
- this._eventListeners = [];
- }
-}
-
-/**
- * Class representing a generic value with an override stack.
- * Changes can be observed by listening to the 'change' event.
- * @template [T=unknown]
- * @augments EventDispatcher<import('dynamic-property').Events<T>>
- */
-export class DynamicProperty extends EventDispatcher {
- /**
- * Creates a new instance with the specified value.
- * @param {T} value The value to assign.
- */
- constructor(value) {
- super();
- /** @type {T} */
- this._value = value;
- /** @type {T} */
- this._defaultValue = value;
- /** @type {{value: T, priority: number, token: string}[]} */
- this._overrides = [];
- }
-
- /**
- * Gets the default value for the property, which is assigned to the
- * public value property when no overrides are present.
- * @type {T}
- */
- get defaultValue() {
- return this._defaultValue;
- }
-
- /**
- * Assigns the default value for the property. If no overrides are present
- * and if the value is different than the current default value,
- * the 'change' event will be triggered.
- * @param {T} value The value to assign.
- */
- set defaultValue(value) {
- this._defaultValue = value;
- if (this._overrides.length === 0) { this._updateValue(); }
- }
-
- /**
- * Gets the current value for the property, taking any overrides into account.
- * @type {T}
- */
- get value() {
- return this._value;
- }
-
- /**
- * Gets the number of overrides added to the property.
- * @type {number}
- */
- get overrideCount() {
- return this._overrides.length;
- }
-
- /**
- * Adds an override value with the specified priority to the override stack.
- * Values with higher priority will take precedence over those with lower.
- * For tie breaks, the override value added first will take precedence.
- * If the newly added override has the highest priority of all overrides
- * and if the override value is different from the current value,
- * the 'change' event will be fired.
- * @param {T} value The override value to assign.
- * @param {number} [priority] The priority value to use, as a number.
- * @returns {import('core').TokenString} A string token which can be passed to the clearOverride function
- * to remove the override.
- */
- setOverride(value, priority = 0) {
- const overridesCount = this._overrides.length;
- let i = 0;
- for (; i < overridesCount; ++i) {
- if (priority > this._overrides[i].priority) { break; }
- }
- const token = generateId(16);
- this._overrides.splice(i, 0, {value, priority, token});
- if (i === 0) { this._updateValue(); }
- return token;
- }
-
- /**
- * Removes a specific override value. If the removed override
- * had the highest priority, and the new value is different from
- * the previous value, the 'change' event will be fired.
- * @param {import('core').TokenString} token The token for the corresponding override which is to be removed.
- * @returns {boolean} `true` if an override was returned, `false` otherwise.
- */
- clearOverride(token) {
- for (let i = 0, ii = this._overrides.length; i < ii; ++i) {
- if (this._overrides[i].token === token) {
- this._overrides.splice(i, 1);
- if (i === 0) { this._updateValue(); }
- return true;
- }
- }
- return false;
- }
-
- /**
- * Updates the current value using the current overrides and default value.
- * If the new value differs from the previous value, the 'change' event will be fired.
- */
- _updateValue() {
- const value = this._overrides.length > 0 ? this._overrides[0].value : this._defaultValue;
- if (this._value === value) { return; }
- this._value = value;
- this.trigger('change', {value});
- }
-}
-
-/**
- * This class handles logging of messages to the console and triggering
- * an event for log calls.
- * @augments EventDispatcher<import('log').Events>
- */
-export class Logger extends EventDispatcher {
- /**
- * Creates a new instance.
- */
- constructor() {
- super();
- /** @type {string} */
- this._extensionName = 'Yomitan';
- try {
- const {name, version} = chrome.runtime.getManifest();
- this._extensionName = `${name} ${version}`;
- } catch (e) {
- // NOP
- }
- }
-
- /**
- * Logs a generic error. This will trigger the 'log' event with the same arguments as the function invocation.
- * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
- * @param {import('log').LogLevel} level The level to log at. Values include `'info'`, `'debug'`, `'warn'`, and `'error'`.
- * Other values will be logged at a non-error level.
- * @param {?import('log').LogContext} [context] An optional context object for the error which should typically include a `url` field.
- */
- log(error, level, context = null) {
- if (typeof context !== 'object' || context === null) {
- context = {url: location.href};
- }
-
- let errorString;
- try {
- if (typeof error === 'string') {
- errorString = error;
- } else {
- errorString = (
- typeof error === 'object' && error !== null ?
- error.toString() :
- `${error}`
- );
- if (/^\[object \w+\]$/.test(errorString)) {
- errorString = JSON.stringify(error);
- }
- }
- } catch (e) {
- errorString = `${error}`;
- }
-
- let errorStack;
- try {
- errorStack = (
- error instanceof Error ?
- (typeof error.stack === 'string' ? error.stack.trimEnd() : '') :
- ''
- );
- } catch (e) {
- errorStack = '';
- }
-
- let errorData;
- try {
- if (error instanceof ExtensionError) {
- errorData = error.data;
- }
- } catch (e) {
- // NOP
- }
-
- if (errorStack.startsWith(errorString)) {
- errorString = errorStack;
- } else if (errorStack.length > 0) {
- errorString += `\n${errorStack}`;
- }
-
- let message = `${this._extensionName} 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/themoeway/yomitan/issues';
-
- /* eslint-disable no-console */
- switch (level) {
- case 'log': console.log(message); break;
- case 'info': console.info(message); break;
- case 'debug': console.debug(message); break;
- case 'warn': console.warn(message); break;
- case 'error': console.error(message); break;
- }
- /* eslint-enable no-console */
-
- this.trigger('log', {error, level, context});
- }
-
- /**
- * Logs a warning. This function invokes `log` internally.
- * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
- * @param {?import('log').LogContext} context An optional context object for the error which should typically include a `url` field.
- */
- warn(error, context = null) {
- this.log(error, 'warn', context);
- }
-
- /**
- * Logs an error. This function invokes `log` internally.
- * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
- * @param {?import('log').LogContext} context An optional context object for the error which should typically include a `url` field.
- */
- error(error, context = null) {
- this.log(error, 'error', context);
- }
-
- /**
- * @param {import('log').LogLevel} errorLevel
- * @returns {import('log').LogErrorLevelValue}
- */
- getLogErrorLevelValue(errorLevel) {
- switch (errorLevel) {
- case 'log':
- case 'info':
- case 'debug':
- return 0;
- case 'warn': return 1;
- case 'error': return 2;
- }
- }
-}
-
-/**
- * This object is the default logger used by the runtime.
- */
-export const log = new Logger();
diff --git a/ext/js/core/dynamic-property.js b/ext/js/core/dynamic-property.js
new file mode 100644
index 00000000..5d8b4716
--- /dev/null
+++ b/ext/js/core/dynamic-property.js
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023-2024 Yomitan Authors
+ * Copyright (C) 2019-2022 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 <https://www.gnu.org/licenses/>.
+ */
+
+import {EventDispatcher} from './event-dispatcher.js';
+import {generateId} from './utilities.js';
+
+/**
+ * Class representing a generic value with an override stack.
+ * Changes can be observed by listening to the 'change' event.
+ * @template [T=unknown]
+ * @augments EventDispatcher<import('dynamic-property').Events<T>>
+ */
+export class DynamicProperty extends EventDispatcher {
+ /**
+ * Creates a new instance with the specified value.
+ * @param {T} value The value to assign.
+ */
+ constructor(value) {
+ super();
+ /** @type {T} */
+ this._value = value;
+ /** @type {T} */
+ this._defaultValue = value;
+ /** @type {{value: T, priority: number, token: string}[]} */
+ this._overrides = [];
+ }
+
+ /**
+ * Gets the default value for the property, which is assigned to the
+ * public value property when no overrides are present.
+ * @type {T}
+ */
+ get defaultValue() {
+ return this._defaultValue;
+ }
+
+ /**
+ * Assigns the default value for the property. If no overrides are present
+ * and if the value is different than the current default value,
+ * the 'change' event will be triggered.
+ * @param {T} value The value to assign.
+ */
+ set defaultValue(value) {
+ this._defaultValue = value;
+ if (this._overrides.length === 0) { this._updateValue(); }
+ }
+
+ /**
+ * Gets the current value for the property, taking any overrides into account.
+ * @type {T}
+ */
+ get value() {
+ return this._value;
+ }
+
+ /**
+ * Gets the number of overrides added to the property.
+ * @type {number}
+ */
+ get overrideCount() {
+ return this._overrides.length;
+ }
+
+ /**
+ * Adds an override value with the specified priority to the override stack.
+ * Values with higher priority will take precedence over those with lower.
+ * For tie breaks, the override value added first will take precedence.
+ * If the newly added override has the highest priority of all overrides
+ * and if the override value is different from the current value,
+ * the 'change' event will be fired.
+ * @param {T} value The override value to assign.
+ * @param {number} [priority] The priority value to use, as a number.
+ * @returns {import('core').TokenString} A string token which can be passed to the clearOverride function
+ * to remove the override.
+ */
+ setOverride(value, priority = 0) {
+ const overridesCount = this._overrides.length;
+ let i = 0;
+ for (; i < overridesCount; ++i) {
+ if (priority > this._overrides[i].priority) { break; }
+ }
+ const token = generateId(16);
+ this._overrides.splice(i, 0, {value, priority, token});
+ if (i === 0) { this._updateValue(); }
+ return token;
+ }
+
+ /**
+ * Removes a specific override value. If the removed override
+ * had the highest priority, and the new value is different from
+ * the previous value, the 'change' event will be fired.
+ * @param {import('core').TokenString} token The token for the corresponding override which is to be removed.
+ * @returns {boolean} `true` if an override was returned, `false` otherwise.
+ */
+ clearOverride(token) {
+ for (let i = 0, ii = this._overrides.length; i < ii; ++i) {
+ if (this._overrides[i].token === token) {
+ this._overrides.splice(i, 1);
+ if (i === 0) { this._updateValue(); }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Updates the current value using the current overrides and default value.
+ * If the new value differs from the previous value, the 'change' event will be fired.
+ */
+ _updateValue() {
+ const value = this._overrides.length > 0 ? this._overrides[0].value : this._defaultValue;
+ if (this._value === value) { return; }
+ this._value = value;
+ this.trigger('change', {value});
+ }
+}
diff --git a/ext/js/core/event-dispatcher.js b/ext/js/core/event-dispatcher.js
new file mode 100644
index 00000000..75f2fdd3
--- /dev/null
+++ b/ext/js/core/event-dispatcher.js
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023-2024 Yomitan Authors
+ * Copyright (C) 2019-2022 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 <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * The following typedef is required because the JSDoc `implements` tag doesn't work with `import()`.
+ * https://github.com/microsoft/TypeScript/issues/49905
+ * @typedef {import('core').EventDispatcherOffGeneric} EventDispatcherOffGeneric
+ */
+
+/**
+ * Base class controls basic event dispatching.
+ * @template {import('core').EventSurface} TSurface
+ * @implements {EventDispatcherOffGeneric}
+ */
+export class EventDispatcher {
+ /**
+ * Creates a new instance.
+ */
+ constructor() {
+ /** @type {Map<import('core').EventNames<TSurface>, import('core').EventHandlerAny[]>} */
+ this._eventMap = new Map();
+ }
+
+ /**
+ * Triggers an event with the given name and specified argument.
+ * @template {import('core').EventNames<TSurface>} TName
+ * @param {TName} eventName The string representing the event's name.
+ * @param {import('core').EventArgument<TSurface, TName>} details The argument passed to the callback functions.
+ * @returns {boolean} `true` if any callbacks were registered, `false` otherwise.
+ */
+ trigger(eventName, details) {
+ const callbacks = this._eventMap.get(eventName);
+ if (typeof callbacks === 'undefined') { return false; }
+
+ for (const callback of callbacks) {
+ callback(details);
+ }
+ return true;
+ }
+
+ /**
+ * Adds a single event listener to a specific event.
+ * @template {import('core').EventNames<TSurface>} TName
+ * @param {TName} eventName The string representing the event's name.
+ * @param {import('core').EventHandler<TSurface, TName>} callback The event listener callback to add.
+ */
+ on(eventName, callback) {
+ let callbacks = this._eventMap.get(eventName);
+ if (typeof callbacks === 'undefined') {
+ callbacks = [];
+ this._eventMap.set(eventName, callbacks);
+ }
+ callbacks.push(callback);
+ }
+
+ /**
+ * Removes a single event listener from a specific event.
+ * @template {import('core').EventNames<TSurface>} TName
+ * @param {TName} eventName The string representing the event's name.
+ * @param {import('core').EventHandler<TSurface, TName>} callback The event listener callback to add.
+ * @returns {boolean} `true` if the callback was removed, `false` otherwise.
+ */
+ off(eventName, callback) {
+ const callbacks = this._eventMap.get(eventName);
+ if (typeof callbacks === 'undefined') { return false; }
+
+ const ii = callbacks.length;
+ for (let i = 0; i < ii; ++i) {
+ if (callbacks[i] === callback) {
+ callbacks.splice(i, 1);
+ if (callbacks.length === 0) {
+ this._eventMap.delete(eventName);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if an event has any listeners.
+ * @template {import('core').EventNames<TSurface>} TName
+ * @param {TName} eventName The string representing the event's name.
+ * @returns {boolean} `true` if the event has listeners, `false` otherwise.
+ */
+ hasListeners(eventName) {
+ const callbacks = this._eventMap.get(eventName);
+ return (typeof callbacks !== 'undefined' && callbacks.length > 0);
+ }
+}
diff --git a/ext/js/core/event-listener-collection.js b/ext/js/core/event-listener-collection.js
new file mode 100644
index 00000000..87d18a90
--- /dev/null
+++ b/ext/js/core/event-listener-collection.js
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023-2024 Yomitan Authors
+ * Copyright (C) 2019-2022 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 <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Class which stores event listeners added to various objects, making it easy to remove them in bulk.
+ */
+export class EventListenerCollection {
+ /**
+ * Creates a new instance.
+ */
+ constructor() {
+ /** @type {import('event-listener-collection').EventListenerDetails[]} */
+ this._eventListeners = [];
+ }
+
+ /**
+ * Returns the number of event listeners that are currently in the object.
+ * @type {number}
+ */
+ get size() {
+ return this._eventListeners.length;
+ }
+
+ /**
+ * Adds an event listener using `object.addEventListener`. The listener will later be removed using `object.removeEventListener`.
+ * @param {import('event-listener-collection').EventTarget} target The object to add the event listener to.
+ * @param {string} type The name of the event.
+ * @param {EventListener | EventListenerObject | import('event-listener-collection').EventListenerFunction} listener The callback listener.
+ * @param {AddEventListenerOptions | boolean} [options] Options for the event.
+ */
+ addEventListener(target, type, listener, options) {
+ target.addEventListener(type, listener, options);
+ this._eventListeners.push({type: 'removeEventListener', target, eventName: type, listener, options});
+ }
+
+ /**
+ * Adds an event listener using `object.addListener`. The listener will later be removed using `object.removeListener`.
+ * @template {import('event-listener-collection').EventListenerFunction} TCallback
+ * @template [TArgs=unknown]
+ * @param {import('event-listener-collection').ExtensionEvent<TCallback, TArgs>} target The object to add the event listener to.
+ * @param {TCallback} callback The callback.
+ * @param {TArgs[]} args The extra argument array passed to the `addListener`/`removeListener` function.
+ */
+ addListener(target, callback, ...args) {
+ target.addListener(callback, ...args);
+ this._eventListeners.push({type: 'removeListener', target, callback, args});
+ }
+
+ /**
+ * Adds an event listener using `object.on`. The listener will later be removed using `object.off`.
+ * @template {import('core').EventSurface} TSurface
+ * @template {import('core').EventNames<TSurface>} TName
+ * @param {import('./event-dispatcher.js').EventDispatcher<TSurface>} target The object to add the event listener to.
+ * @param {TName} eventName The string representing the event's name.
+ * @param {import('core').EventHandler<TSurface, TName>} callback The event listener callback to add.
+ */
+ on(target, eventName, callback) {
+ target.on(eventName, callback);
+ this._eventListeners.push({type: 'off', eventName, target, callback});
+ }
+
+ /**
+ * Removes all event listeners added to objects for this instance and clears the internal list of event listeners.
+ */
+ removeAllEventListeners() {
+ if (this._eventListeners.length === 0) { return; }
+ for (const item of this._eventListeners) {
+ switch (item.type) {
+ case 'removeEventListener':
+ item.target.removeEventListener(item.eventName, item.listener, item.options);
+ break;
+ case 'removeListener':
+ item.target.removeListener(item.callback, ...item.args);
+ break;
+ case 'off':
+ item.target.off(item.eventName, item.callback);
+ break;
+ }
+ }
+ this._eventListeners = [];
+ }
+}
diff --git a/ext/js/core/logger.js b/ext/js/core/logger.js
new file mode 100644
index 00000000..165e1ae2
--- /dev/null
+++ b/ext/js/core/logger.js
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2023-2024 Yomitan Authors
+ * Copyright (C) 2019-2022 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 <https://www.gnu.org/licenses/>.
+ */
+
+import {EventDispatcher} from './event-dispatcher.js';
+import {ExtensionError} from './extension-error.js';
+
+/**
+ * This class handles logging of messages to the console and triggering
+ * an event for log calls.
+ * @augments EventDispatcher<import('log').Events>
+ */
+export class Logger extends EventDispatcher {
+ /**
+ * Creates a new instance.
+ */
+ constructor() {
+ super();
+ /** @type {string} */
+ this._extensionName = 'Yomitan';
+ try {
+ const {name, version} = chrome.runtime.getManifest();
+ this._extensionName = `${name} ${version}`;
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ /**
+ * Logs a generic error. This will trigger the 'log' event with the same arguments as the function invocation.
+ * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
+ * @param {import('log').LogLevel} level The level to log at. Values include `'info'`, `'debug'`, `'warn'`, and `'error'`.
+ * Other values will be logged at a non-error level.
+ * @param {?import('log').LogContext} [context] An optional context object for the error which should typically include a `url` field.
+ */
+ log(error, level, context = null) {
+ if (typeof context !== 'object' || context === null) {
+ context = {url: location.href};
+ }
+
+ let errorString;
+ try {
+ if (typeof error === 'string') {
+ errorString = error;
+ } else {
+ errorString = (
+ typeof error === 'object' && error !== null ?
+ error.toString() :
+ `${error}`
+ );
+ if (/^\[object \w+\]$/.test(errorString)) {
+ errorString = JSON.stringify(error);
+ }
+ }
+ } catch (e) {
+ errorString = `${error}`;
+ }
+
+ let errorStack;
+ try {
+ errorStack = (
+ error instanceof Error ?
+ (typeof error.stack === 'string' ? error.stack.trimEnd() : '') :
+ ''
+ );
+ } catch (e) {
+ errorStack = '';
+ }
+
+ let errorData;
+ try {
+ if (error instanceof ExtensionError) {
+ errorData = error.data;
+ }
+ } catch (e) {
+ // NOP
+ }
+
+ if (errorStack.startsWith(errorString)) {
+ errorString = errorStack;
+ } else if (errorStack.length > 0) {
+ errorString += `\n${errorStack}`;
+ }
+
+ let message = `${this._extensionName} 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/themoeway/yomitan/issues';
+
+ /* eslint-disable no-console */
+ switch (level) {
+ case 'log': console.log(message); break;
+ case 'info': console.info(message); break;
+ case 'debug': console.debug(message); break;
+ case 'warn': console.warn(message); break;
+ case 'error': console.error(message); break;
+ }
+ /* eslint-enable no-console */
+
+ this.trigger('log', {error, level, context});
+ }
+
+ /**
+ * Logs a warning. This function invokes `log` internally.
+ * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
+ * @param {?import('log').LogContext} context An optional context object for the error which should typically include a `url` field.
+ */
+ warn(error, context = null) {
+ this.log(error, 'warn', context);
+ }
+
+ /**
+ * Logs an error. This function invokes `log` internally.
+ * @param {unknown} error The error to log. This is typically an `Error` or `Error`-like object.
+ * @param {?import('log').LogContext} context An optional context object for the error which should typically include a `url` field.
+ */
+ error(error, context = null) {
+ this.log(error, 'error', context);
+ }
+
+ /**
+ * @param {import('log').LogLevel} errorLevel
+ * @returns {import('log').LogErrorLevelValue}
+ */
+ getLogErrorLevelValue(errorLevel) {
+ switch (errorLevel) {
+ case 'log':
+ case 'info':
+ case 'debug':
+ return 0;
+ case 'warn': return 1;
+ case 'error': return 2;
+ }
+ }
+}
+
+/**
+ * This object is the default logger used by the runtime.
+ */
+export const log = new Logger();
diff --git a/ext/js/core/utilities.js b/ext/js/core/utilities.js
new file mode 100644
index 00000000..1fc2da42
--- /dev/null
+++ b/ext/js/core/utilities.js
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2023-2024 Yomitan Authors
+ * Copyright (C) 2019-2022 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 <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Checks whether a given value is a non-array object.
+ * @param {unknown} value The value to check.
+ * @returns {boolean} `true` if the value is an object and not an array, `false` otherwise.
+ */
+export function isObject(value) {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+/**
+ * Converts any string into a form that can be passed into the RegExp constructor.
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+ * @param {string} string The string to convert to a valid regular expression.
+ * @returns {string} The escaped string.
+ */
+export function escapeRegExp(string) {
+ return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
+}
+
+/**
+ * Reverses a string.
+ * @param {string} string The string to reverse.
+ * @returns {string} The returned string, which retains proper UTF-16 surrogate pair order.
+ */
+export function stringReverse(string) {
+ return [...string].reverse().join('');
+}
+
+/**
+ * Creates a deep clone of an object or value. This is similar to `parseJson(JSON.stringify(value))`.
+ * @template [T=unknown]
+ * @param {T} value The value to clone.
+ * @returns {T} A new clone of the value.
+ * @throws An error if the value is circular and cannot be cloned.
+ */
+export function clone(value) {
+ if (value === null) { return /** @type {T} */ (null); }
+ switch (typeof value) {
+ case 'boolean':
+ case 'number':
+ case 'string':
+ case 'bigint':
+ case 'symbol':
+ case 'undefined':
+ return value;
+ default:
+ return cloneInternal(value, new Set());
+ }
+}
+
+/**
+ * @template [T=unknown]
+ * @param {T} value
+ * @param {Set<unknown>} visited
+ * @returns {T}
+ * @throws {Error}
+ */
+function cloneInternal(value, visited) {
+ if (value === null) { return /** @type {T} */ (null); }
+ switch (typeof value) {
+ case 'boolean':
+ case 'number':
+ case 'string':
+ case 'bigint':
+ case 'symbol':
+ case 'undefined':
+ return value;
+ case 'object':
+ return /** @type {T} */ (
+ Array.isArray(value) ?
+ cloneArray(value, visited) :
+ cloneObject(/** @type {import('core').SerializableObject} */ (value), visited)
+ );
+ default:
+ throw new Error(`Cannot clone object of type ${typeof value}`);
+ }
+}
+
+/**
+ * @param {unknown[]} value
+ * @param {Set<unknown>} visited
+ * @returns {unknown[]}
+ * @throws {Error}
+ */
+function cloneArray(value, visited) {
+ if (visited.has(value)) { throw new Error('Circular'); }
+ try {
+ visited.add(value);
+ const result = [];
+ for (const item of value) {
+ result.push(cloneInternal(item, visited));
+ }
+ return result;
+ } finally {
+ visited.delete(value);
+ }
+}
+
+/**
+ * @param {import('core').SerializableObject} value
+ * @param {Set<unknown>} visited
+ * @returns {import('core').SerializableObject}
+ * @throws {Error}
+ */
+function cloneObject(value, visited) {
+ if (visited.has(value)) { throw new Error('Circular'); }
+ try {
+ visited.add(value);
+ /** @type {import('core').SerializableObject} */
+ const result = {};
+ for (const key in value) {
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
+ result[key] = cloneInternal(value[key], visited);
+ }
+ }
+ return result;
+ } finally {
+ visited.delete(value);
+ }
+}
+
+/**
+ * Checks if an object or value is deeply equal to another object or value.
+ * @param {unknown} value1 The first value to check.
+ * @param {unknown} value2 The second value to check.
+ * @returns {boolean} `true` if the values are the same object, or deeply equal without cycles. `false` otherwise.
+ */
+export function deepEqual(value1, value2) {
+ if (value1 === value2) { return true; }
+
+ const type = typeof value1;
+ if (typeof value2 !== type) { return false; }
+
+ switch (type) {
+ case 'object':
+ case 'function':
+ return deepEqualInternal(value1, value2, new Set());
+ default:
+ return false;
+ }
+}
+
+/**
+ * @param {unknown} value1
+ * @param {unknown} value2
+ * @param {Set<unknown>} visited1
+ * @returns {boolean}
+ */
+function deepEqualInternal(value1, value2, visited1) {
+ if (value1 === value2) { return true; }
+
+ const type = typeof value1;
+ if (typeof value2 !== type) { return false; }
+
+ switch (type) {
+ case 'object':
+ case 'function':
+ {
+ if (value1 === null || value2 === null) { return false; }
+ const array = Array.isArray(value1);
+ if (array !== Array.isArray(value2)) { return false; }
+ if (visited1.has(value1)) { return false; }
+ visited1.add(value1);
+ return (
+ array ?
+ areArraysEqual(/** @type {unknown[]} */ (value1), /** @type {unknown[]} */ (value2), visited1) :
+ areObjectsEqual(/** @type {import('core').UnknownObject} */ (value1), /** @type {import('core').UnknownObject} */ (value2), visited1)
+ );
+ }
+ default:
+ return false;
+ }
+}
+
+/**
+ * @param {import('core').UnknownObject} value1
+ * @param {import('core').UnknownObject} value2
+ * @param {Set<unknown>} visited1
+ * @returns {boolean}
+ */
+function areObjectsEqual(value1, value2, visited1) {
+ const keys1 = Object.keys(value1);
+ const keys2 = Object.keys(value2);
+ if (keys1.length !== keys2.length) { return false; }
+
+ const keys1Set = new Set(keys1);
+ for (const key of keys2) {
+ if (!keys1Set.has(key) || !deepEqualInternal(value1[key], value2[key], visited1)) { return false; }
+ }
+
+ return true;
+}
+
+/**
+ * @param {unknown[]} value1
+ * @param {unknown[]} value2
+ * @param {Set<unknown>} visited1
+ * @returns {boolean}
+ */
+function areArraysEqual(value1, value2, visited1) {
+ const length = value1.length;
+ if (length !== value2.length) { return false; }
+
+ for (let i = 0; i < length; ++i) {
+ if (!deepEqualInternal(value1[i], value2[i], visited1)) { return false; }
+ }
+
+ return true;
+}
+
+/**
+ * Creates a new base-16 (lower case) string of a sequence of random bytes of the given length.
+ * @param {number} length The number of bytes the string represents. The returned string's length will be twice as long.
+ * @returns {string} A string of random characters.
+ */
+export function 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;
+}
+
+/**
+ * Creates an unresolved promise that can be resolved later, outside the promise's executor function.
+ * @template [T=unknown]
+ * @returns {import('core').DeferredPromiseDetails<T>} An object `{promise, resolve, reject}`, containing the promise and the resolve/reject functions.
+ */
+export function deferPromise() {
+ /** @type {((value: T) => void)|undefined} */
+ let resolve;
+ /** @type {((reason?: import('core').RejectionReason) => void)|undefined} */
+ let reject;
+ const promise = new Promise((resolve2, reject2) => {
+ resolve = resolve2;
+ reject = reject2;
+ });
+ return {
+ promise,
+ resolve: /** @type {(value: T) => void} */ (resolve),
+ reject: /** @type {(reason?: import('core').RejectionReason) => void} */ (reject)
+ };
+}
+
+/**
+ * Creates a promise that is resolved after a set delay.
+ * @param {number} delay How many milliseconds until the promise should be resolved. If 0, the promise is immediately resolved.
+ * @returns {Promise<void>} A promise with two additional properties: `resolve` and `reject`, which can be used to complete the promise early.
+ */
+export function promiseTimeout(delay) {
+ return delay <= 0 ? Promise.resolve() : new Promise((resolve) => { setTimeout(resolve, delay); });
+}
+
+/**
+ * Creates a promise that will resolve after the next animation frame, using `requestAnimationFrame`.
+ * @param {number} [timeout] A maximum duration (in milliseconds) to wait until the promise resolves. If null or omitted, no timeout is used.
+ * @returns {Promise<{time: number, timeout: boolean}>} A promise that is resolved with `{time, timeout}`, where `time` is the timestamp from `requestAnimationFrame`,
+ * and `timeout` is a boolean indicating whether the cause was a timeout or not.
+ * @throws The promise throws an error if animation is not supported in this context, such as in a service worker.
+ */
+export function promiseAnimationFrame(timeout) {
+ return new Promise((resolve, reject) => {
+ if (typeof cancelAnimationFrame !== 'function' || typeof requestAnimationFrame !== 'function') {
+ reject(new Error('Animation not supported in this context'));
+ return;
+ }
+
+ /** @type {?import('core').Timeout} */
+ let timer = null;
+ /** @type {?number} */
+ let frameRequest = null;
+ /**
+ * @param {number} time
+ */
+ const onFrame = (time) => {
+ frameRequest = null;
+ if (timer !== null) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ resolve({time, timeout: false});
+ };
+ const onTimeout = () => {
+ timer = null;
+ if (frameRequest !== null) {
+ // eslint-disable-next-line no-undef
+ cancelAnimationFrame(frameRequest);
+ frameRequest = null;
+ }
+ resolve({time: performance.now(), timeout: true});
+ };
+
+ // eslint-disable-next-line no-undef
+ frameRequest = requestAnimationFrame(onFrame);
+ if (typeof timeout === 'number') {
+ timer = setTimeout(onTimeout, timeout);
+ }
+ });
+}
diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js
index 8577205e..48564d54 100644
--- a/ext/js/data/anki-note-builder.js
+++ b/ext/js/data/anki-note-builder.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {deferPromise} from '../core.js';
+import {deferPromise} from '../core/utilities.js';
import {ExtensionError} from '../core/extension-error.js';
import {yomitan} from '../yomitan.js';
import {AnkiUtil} from './anki-util.js';
diff --git a/ext/js/data/anki-util.js b/ext/js/data/anki-util.js
index 1edbc58c..57684887 100644
--- a/ext/js/data/anki-util.js
+++ b/ext/js/data/anki-util.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {isObject} from '../core.js';
+import {isObject} from '../core/utilities.js';
/**
* This class has some general utility functions for working with Anki data.
diff --git a/ext/js/data/json-schema.js b/ext/js/data/json-schema.js
index 350ebbe5..9b7ea011 100644
--- a/ext/js/data/json-schema.js
+++ b/ext/js/data/json-schema.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {clone} from '../core.js';
+import {clone} from '../core/utilities.js';
import {CacheMap} from '../general/cache-map.js';
export class JsonSchemaError extends Error {
diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js
index 42f82ca2..cbaeb92b 100644
--- a/ext/js/data/options-util.js
+++ b/ext/js/data/options-util.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {escapeRegExp, isObject} from '../core.js';
+import {escapeRegExp, isObject} from '../core/utilities.js';
import {parseJson, readResponseJson} from '../core/json.js';
import {TemplatePatcher} from '../templates/template-patcher.js';
import {JsonSchema} from './json-schema.js';
diff --git a/ext/js/dictionary/dictionary-database.js b/ext/js/dictionary/dictionary-database.js
index 818392f4..1c52b7ab 100644
--- a/ext/js/dictionary/dictionary-database.js
+++ b/ext/js/dictionary/dictionary-database.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log, stringReverse} from '../core.js';
+import {log} from '../core/logger.js';
+import {stringReverse} from '../core/utilities.js';
import {Database} from '../data/database.js';
export class DictionaryDatabase {
diff --git a/ext/js/dictionary/dictionary-importer-media-loader.js b/ext/js/dictionary/dictionary-importer-media-loader.js
index d94ff98e..86ed53f2 100644
--- a/ext/js/dictionary/dictionary-importer-media-loader.js
+++ b/ext/js/dictionary/dictionary-importer-media-loader.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
/**
* Class used for loading and validating media during the dictionary import process.
diff --git a/ext/js/dictionary/dictionary-importer.js b/ext/js/dictionary/dictionary-importer.js
index 067c8a3a..c8f68d78 100644
--- a/ext/js/dictionary/dictionary-importer.js
+++ b/ext/js/dictionary/dictionary-importer.js
@@ -24,7 +24,7 @@ import {
ZipReader as ZipReader0,
configure
} from '../../lib/zip.js';
-import {stringReverse} from '../core.js';
+import {stringReverse} from '../core/utilities.js';
import {ExtensionError} from '../core/extension-error.js';
import {parseJson} from '../core/json.js';
import {toError} from '../core/to-error.js';
diff --git a/ext/js/dictionary/dictionary-worker-main.js b/ext/js/dictionary/dictionary-worker-main.js
index 16d013af..d63a3a20 100644
--- a/ext/js/dictionary/dictionary-worker-main.js
+++ b/ext/js/dictionary/dictionary-worker-main.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log} from '../core.js';
+import {log} from '../core/logger.js';
import {DictionaryWorkerHandler} from './dictionary-worker-handler.js';
/** Entry point. */
diff --git a/ext/js/dictionary/dictionary-worker-media-loader.js b/ext/js/dictionary/dictionary-worker-media-loader.js
index cc380c2f..5c18e184 100644
--- a/ext/js/dictionary/dictionary-worker-media-loader.js
+++ b/ext/js/dictionary/dictionary-worker-media-loader.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {generateId} from '../core.js';
+import {generateId} from '../core/utilities.js';
import {ExtensionError} from '../core/extension-error.js';
/**
diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js
index 2c24bafb..c51ddfa2 100644
--- a/ext/js/display/display-anki.js
+++ b/ext/js/display/display-anki.js
@@ -16,8 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection, deferPromise} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {toError} from '../core/to-error.js';
+import {deferPromise} from '../core/utilities.js';
import {AnkiNoteBuilder} from '../data/anki-note-builder.js';
import {AnkiUtil} from '../data/anki-util.js';
import {PopupMenu} from '../dom/popup-menu.js';
diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js
index 55f83a33..8cbfc83f 100644
--- a/ext/js/display/display-audio.js
+++ b/ext/js/display/display-audio.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {PopupMenu} from '../dom/popup-menu.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
import {AudioSystem} from '../media/audio-system.js';
diff --git a/ext/js/display/display-content-manager.js b/ext/js/display/display-content-manager.js
index 98b40aae..d13dffb3 100644
--- a/ext/js/display/display-content-manager.js
+++ b/ext/js/display/display-content-manager.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js
index 96559910..7bf13b77 100644
--- a/ext/js/display/display-generator.js
+++ b/ext/js/display/display-generator.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {isObject} from '../core.js';
+import {isObject} from '../core/utilities.js';
import {ExtensionError} from '../core/extension-error.js';
import {DictionaryDataUtil} from '../dictionary/dictionary-data-util.js';
import {HtmlTemplateCollection} from '../dom/html-template-collection.js';
diff --git a/ext/js/display/display-history.js b/ext/js/display/display-history.js
index 021a7bd0..255a8536 100644
--- a/ext/js/display/display-history.js
+++ b/ext/js/display/display-history.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, generateId, isObject} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {generateId, isObject} from '../core/utilities.js';
/**
* @augments EventDispatcher<import('display-history').Events>
diff --git a/ext/js/display/display-notification.js b/ext/js/display/display-notification.js
index 6d7cb63e..a955f3fa 100644
--- a/ext/js/display/display-notification.js
+++ b/ext/js/display/display-notification.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
export class DisplayNotification {
diff --git a/ext/js/display/display-profile-selection.js b/ext/js/display/display-profile-selection.js
index 04d6d9e1..ff4fe6bf 100644
--- a/ext/js/display/display-profile-selection.js
+++ b/ext/js/display/display-profile-selection.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection, generateId} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
+import {generateId} from '../core/utilities.js';
import {PanelElement} from '../dom/panel-element.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/display/display-resizer.js b/ext/js/display/display-resizer.js
index 009b2195..7e346c7d 100644
--- a/ext/js/display/display-resizer.js
+++ b/ext/js/display/display-resizer.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
export class DisplayResizer {
/**
diff --git a/ext/js/display/display.js b/ext/js/display/display.js
index c7d58497..677c7c4b 100644
--- a/ext/js/display/display.js
+++ b/ext/js/display/display.js
@@ -18,10 +18,14 @@
import {ThemeController} from '../app/theme-controller.js';
import {FrameEndpoint} from '../comm/frame-endpoint.js';
-import {DynamicProperty, EventDispatcher, EventListenerCollection, clone, deepEqual, log, promiseTimeout} from '../core.js';
import {extendApiMap, invokeApiMapHandler} from '../core/api-map.js';
+import {DynamicProperty} from '../core/dynamic-property.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {ExtensionError} from '../core/extension-error.js';
+import {log} from '../core/logger.js';
import {toError} from '../core/to-error.js';
+import {clone, deepEqual, promiseTimeout} from '../core/utilities.js';
import {PopupMenu} from '../dom/popup-menu.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
import {ScrollElement} from '../dom/scroll-element.js';
diff --git a/ext/js/display/element-overflow-controller.js b/ext/js/display/element-overflow-controller.js
index 16ac4a7c..e0b9035e 100644
--- a/ext/js/display/element-overflow-controller.js
+++ b/ext/js/display/element-overflow-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../core.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
export class ElementOverflowController {
constructor() {
diff --git a/ext/js/display/option-toggle-hotkey-handler.js b/ext/js/display/option-toggle-hotkey-handler.js
index d06be649..d9065e7f 100644
--- a/ext/js/display/option-toggle-hotkey-handler.js
+++ b/ext/js/display/option-toggle-hotkey-handler.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {generateId} from '../core.js';
+import {generateId} from '../core/utilities.js';
import {ExtensionError} from '../core/extension-error.js';
import {toError} from '../core/to-error.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/display/popup-main.js b/ext/js/display/popup-main.js
index 251f35dc..d4f622f2 100644
--- a/ext/js/display/popup-main.js
+++ b/ext/js/display/popup-main.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log} from '../core.js';
+import {log} from '../core/logger.js';
import {DocumentFocusController} from '../dom/document-focus-controller.js';
import {HotkeyHandler} from '../input/hotkey-handler.js';
import {JapaneseUtil} from '../language/sandbox/japanese-util.js';
diff --git a/ext/js/display/query-parser.js b/ext/js/display/query-parser.js
index c7ab77bf..e129e1be 100644
--- a/ext/js/display/query-parser.js
+++ b/ext/js/display/query-parser.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, log} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {log} from '../core/logger.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
import {TextScanner} from '../language/text-scanner.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js
index 638bc52c..594a80aa 100644
--- a/ext/js/display/search-display-controller.js
+++ b/ext/js/display/search-display-controller.js
@@ -18,8 +18,8 @@
import * as wanakana from '../../lib/wanakana.js';
import {ClipboardMonitor} from '../comm/clipboard-monitor.js';
-import {EventListenerCollection} from '../core.js';
import {createApiMap, invokeApiMapHandler} from '../core/api-map.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/display/search-main.js b/ext/js/display/search-main.js
index 4484f47c..3cdd1f25 100644
--- a/ext/js/display/search-main.js
+++ b/ext/js/display/search-main.js
@@ -17,7 +17,7 @@
*/
import * as wanakana from '../../lib/wanakana.js';
-import {log} from '../core.js';
+import {log} from '../core/logger.js';
import {DocumentFocusController} from '../dom/document-focus-controller.js';
import {HotkeyHandler} from '../input/hotkey-handler.js';
import {JapaneseUtil} from '../language/sandbox/japanese-util.js';
diff --git a/ext/js/display/search-persistent-state-controller.js b/ext/js/display/search-persistent-state-controller.js
index d34f8502..d1220184 100644
--- a/ext/js/display/search-persistent-state-controller.js
+++ b/ext/js/display/search-persistent-state-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
/**
* @augments EventDispatcher<import('search-persistent-state-controller').Events>
diff --git a/ext/js/dom/document-util.js b/ext/js/dom/document-util.js
index dc5393bf..235a42d0 100644
--- a/ext/js/dom/document-util.js
+++ b/ext/js/dom/document-util.js
@@ -386,7 +386,7 @@ export class DocumentUtil {
/**
* Adds a fullscreen change event listener. This function handles all of the browser-specific variants.
* @param {EventListener} onFullscreenChanged The event callback.
- * @param {?import('../core.js').EventListenerCollection} eventListenerCollection An optional `EventListenerCollection` to add the registration to.
+ * @param {?import('../core/event-listener-collection.js').EventListenerCollection} eventListenerCollection An optional `EventListenerCollection` to add the registration to.
*/
static addFullscreenChangeEventListener(onFullscreenChanged, eventListenerCollection = null) {
const target = document;
diff --git a/ext/js/dom/panel-element.js b/ext/js/dom/panel-element.js
index 8d05bae5..9c1289d7 100644
--- a/ext/js/dom/panel-element.js
+++ b/ext/js/dom/panel-element.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
/**
* @augments EventDispatcher<import('panel-element').Events>
diff --git a/ext/js/dom/popup-menu.js b/ext/js/dom/popup-menu.js
index 94b896f6..8a8a19ba 100644
--- a/ext/js/dom/popup-menu.js
+++ b/ext/js/dom/popup-menu.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, EventListenerCollection} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {querySelectorNotNull} from './query-selector.js';
/**
diff --git a/ext/js/general/task-accumulator.js b/ext/js/general/task-accumulator.js
index bad20ffa..62f12869 100644
--- a/ext/js/general/task-accumulator.js
+++ b/ext/js/general/task-accumulator.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log} from '../core.js';
+import {log} from '../core/logger.js';
/**
* @template [K=unknown]
diff --git a/ext/js/input/hotkey-handler.js b/ext/js/input/hotkey-handler.js
index 6999e4de..3b40a86d 100644
--- a/ext/js/input/hotkey-handler.js
+++ b/ext/js/input/hotkey-handler.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, EventListenerCollection} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
import {DocumentUtil} from '../dom/document-util.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/input/hotkey-help-controller.js b/ext/js/input/hotkey-help-controller.js
index 62312298..4c4f56d5 100644
--- a/ext/js/input/hotkey-help-controller.js
+++ b/ext/js/input/hotkey-help-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {isObject} from '../core.js';
+import {isObject} from '../core/utilities.js';
import {parseJson} from '../core/json.js';
import {yomitan} from '../yomitan.js';
import {HotkeyUtil} from './hotkey-util.js';
diff --git a/ext/js/language/text-scanner.js b/ext/js/language/text-scanner.js
index 639cf7d6..accb53fd 100644
--- a/ext/js/language/text-scanner.js
+++ b/ext/js/language/text-scanner.js
@@ -16,7 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, EventListenerCollection, clone, log} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
+import {EventListenerCollection} from '../core/event-listener-collection.js';
+import {log} from '../core/logger.js';
+import {clone} from '../core/utilities.js';
import {DocumentUtil} from '../dom/document-util.js';
import {TextSourceElement} from '../dom/text-source-element.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/media/audio-system.js b/ext/js/media/audio-system.js
index b479783f..3d861d35 100644
--- a/ext/js/media/audio-system.js
+++ b/ext/js/media/audio-system.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher} from '../core.js';
+import {EventDispatcher} from '../core/event-dispatcher.js';
import {TextToSpeechAudio} from './text-to-speech-audio.js';
/**
diff --git a/ext/js/pages/info-main.js b/ext/js/pages/info-main.js
index 33c82c8a..dd55ab4b 100644
--- a/ext/js/pages/info-main.js
+++ b/ext/js/pages/info-main.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log, promiseTimeout} from '../core.js';
+import {log} from '../core/logger.js';
+import {promiseTimeout} from '../core/utilities.js';
import {DocumentFocusController} from '../dom/document-focus-controller.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/pages/permissions-main.js b/ext/js/pages/permissions-main.js
index 82b8b653..38135689 100644
--- a/ext/js/pages/permissions-main.js
+++ b/ext/js/pages/permissions-main.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log, promiseTimeout} from '../core.js';
+import {log} from '../core/logger.js';
+import {promiseTimeout} from '../core/utilities.js';
import {DocumentFocusController} from '../dom/document-focus-controller.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js
index 69e44aa8..f6e19e14 100644
--- a/ext/js/pages/settings/anki-controller.js
+++ b/ext/js/pages/settings/anki-controller.js
@@ -17,8 +17,9 @@
*/
import {AnkiConnect} from '../../comm/anki-connect.js';
-import {EventListenerCollection, log} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {ExtensionError} from '../../core/extension-error.js';
+import {log} from '../../core/logger.js';
import {toError} from '../../core/to-error.js';
import {AnkiUtil} from '../../data/anki-util.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
diff --git a/ext/js/pages/settings/audio-controller.js b/ext/js/pages/settings/audio-controller.js
index 0b3a1b43..9633c4b3 100644
--- a/ext/js/pages/settings/audio-controller.js
+++ b/ext/js/pages/settings/audio-controller.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, EventListenerCollection} from '../../core.js';
+import {EventDispatcher} from '../../core/event-dispatcher.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {AudioSystem} from '../../media/audio-system.js';
diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js
index ffc8384e..f2eccd1e 100644
--- a/ext/js/pages/settings/backup-controller.js
+++ b/ext/js/pages/settings/backup-controller.js
@@ -17,9 +17,10 @@
*/
import {Dexie} from '../../../lib/dexie.js';
-import {isObject, log} from '../../core.js';
import {parseJson} from '../../core/json.js';
+import {log} from '../../core/logger.js';
import {toError} from '../../core/to-error.js';
+import {isObject} from '../../core/utilities.js';
import {OptionsUtil} from '../../data/options-util.js';
import {ArrayBufferUtil} from '../../data/sandbox/array-buffer-util.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
diff --git a/ext/js/pages/settings/collapsible-dictionary-controller.js b/ext/js/pages/settings/collapsible-dictionary-controller.js
index ab0ecd74..e6930049 100644
--- a/ext/js/pages/settings/collapsible-dictionary-controller.js
+++ b/ext/js/pages/settings/collapsible-dictionary-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {yomitan} from '../../yomitan.js';
diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js
index 25d87707..10dfdcdc 100644
--- a/ext/js/pages/settings/dictionary-controller.js
+++ b/ext/js/pages/settings/dictionary-controller.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection, log} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
+import {log} from '../../core/logger.js';
import {DictionaryWorker} from '../../dictionary/dictionary-worker.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {yomitan} from '../../yomitan.js';
diff --git a/ext/js/pages/settings/dictionary-import-controller.js b/ext/js/pages/settings/dictionary-import-controller.js
index bf878085..183c0ccd 100644
--- a/ext/js/pages/settings/dictionary-import-controller.js
+++ b/ext/js/pages/settings/dictionary-import-controller.js
@@ -16,8 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log} from '../../core.js';
import {ExtensionError} from '../../core/extension-error.js';
+import {log} from '../../core/logger.js';
import {toError} from '../../core/to-error.js';
import {DictionaryWorker} from '../../dictionary/dictionary-worker.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
diff --git a/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js b/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js
index 92422ce7..61330bb8 100644
--- a/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js
+++ b/ext/js/pages/settings/extension-keyboard-shortcuts-controller.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection, isObject} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
+import {isObject} from '../../core/utilities.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {HotkeyUtil} from '../../input/hotkey-util.js';
import {yomitan} from '../../yomitan.js';
diff --git a/ext/js/pages/settings/keyboard-mouse-input-field.js b/ext/js/pages/settings/keyboard-mouse-input-field.js
index 89f77b4a..310cbb19 100644
--- a/ext/js/pages/settings/keyboard-mouse-input-field.js
+++ b/ext/js/pages/settings/keyboard-mouse-input-field.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, EventListenerCollection} from '../../core.js';
+import {EventDispatcher} from '../../core/event-dispatcher.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {DocumentUtil} from '../../dom/document-util.js';
import {HotkeyUtil} from '../../input/hotkey-util.js';
diff --git a/ext/js/pages/settings/keyboard-shortcuts-controller.js b/ext/js/pages/settings/keyboard-shortcuts-controller.js
index 700fe54a..396b0cc2 100644
--- a/ext/js/pages/settings/keyboard-shortcuts-controller.js
+++ b/ext/js/pages/settings/keyboard-shortcuts-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {DocumentUtil} from '../../dom/document-util.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {ObjectPropertyAccessor} from '../../general/object-property-accessor.js';
diff --git a/ext/js/pages/settings/permissions-origin-controller.js b/ext/js/pages/settings/permissions-origin-controller.js
index 73b32e9c..a0f23af6 100644
--- a/ext/js/pages/settings/permissions-origin-controller.js
+++ b/ext/js/pages/settings/permissions-origin-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {toError} from '../../core/to-error.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
diff --git a/ext/js/pages/settings/persistent-storage-controller.js b/ext/js/pages/settings/persistent-storage-controller.js
index 2a6d7d51..baffa969 100644
--- a/ext/js/pages/settings/persistent-storage-controller.js
+++ b/ext/js/pages/settings/persistent-storage-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {isObject} from '../../core.js';
+import {isObject} from '../../core/utilities.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {yomitan} from '../../yomitan.js';
diff --git a/ext/js/pages/settings/popup-preview-frame-main.js b/ext/js/pages/settings/popup-preview-frame-main.js
index 83428b31..e3d7d0ec 100644
--- a/ext/js/pages/settings/popup-preview-frame-main.js
+++ b/ext/js/pages/settings/popup-preview-frame-main.js
@@ -17,7 +17,7 @@
*/
import {PopupFactory} from '../../app/popup-factory.js';
-import {log} from '../../core.js';
+import {log} from '../../core/logger.js';
import {HotkeyHandler} from '../../input/hotkey-handler.js';
import {yomitan} from '../../yomitan.js';
import {PopupPreviewFrame} from './popup-preview-frame.js';
diff --git a/ext/js/pages/settings/profile-conditions-ui.js b/ext/js/pages/settings/profile-conditions-ui.js
index eac33004..d07751fb 100644
--- a/ext/js/pages/settings/profile-conditions-ui.js
+++ b/ext/js/pages/settings/profile-conditions-ui.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, EventListenerCollection} from '../../core.js';
+import {EventDispatcher} from '../../core/event-dispatcher.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {DocumentUtil} from '../../dom/document-util.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {KeyboardMouseInputField} from './keyboard-mouse-input-field.js';
diff --git a/ext/js/pages/settings/profile-controller.js b/ext/js/pages/settings/profile-controller.js
index bf8c2ae4..73926a69 100644
--- a/ext/js/pages/settings/profile-controller.js
+++ b/ext/js/pages/settings/profile-controller.js
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {clone, EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
+import {clone} from '../../core/utilities.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {yomitan} from '../../yomitan.js';
import {ProfileConditionsUI} from './profile-conditions-ui.js';
diff --git a/ext/js/pages/settings/recommended-permissions-controller.js b/ext/js/pages/settings/recommended-permissions-controller.js
index dd93582e..84a4ef10 100644
--- a/ext/js/pages/settings/recommended-permissions-controller.js
+++ b/ext/js/pages/settings/recommended-permissions-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {toError} from '../../core/to-error.js';
export class RecommendedPermissionsController {
diff --git a/ext/js/pages/settings/scan-inputs-controller.js b/ext/js/pages/settings/scan-inputs-controller.js
index 2fb1082d..2dfa3de3 100644
--- a/ext/js/pages/settings/scan-inputs-controller.js
+++ b/ext/js/pages/settings/scan-inputs-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {DocumentUtil} from '../../dom/document-util.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {yomitan} from '../../yomitan.js';
diff --git a/ext/js/pages/settings/secondary-search-dictionary-controller.js b/ext/js/pages/settings/secondary-search-dictionary-controller.js
index 8e637890..592f5eeb 100644
--- a/ext/js/pages/settings/secondary-search-dictionary-controller.js
+++ b/ext/js/pages/settings/secondary-search-dictionary-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {yomitan} from '../../yomitan.js';
diff --git a/ext/js/pages/settings/sentence-termination-characters-controller.js b/ext/js/pages/settings/sentence-termination-characters-controller.js
index 1f982dec..c393aaa1 100644
--- a/ext/js/pages/settings/sentence-termination-characters-controller.js
+++ b/ext/js/pages/settings/sentence-termination-characters-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
export class SentenceTerminationCharactersController {
diff --git a/ext/js/pages/settings/settings-controller.js b/ext/js/pages/settings/settings-controller.js
index f448adcf..25f5e8ad 100644
--- a/ext/js/pages/settings/settings-controller.js
+++ b/ext/js/pages/settings/settings-controller.js
@@ -16,7 +16,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventDispatcher, EventListenerCollection, generateId, isObject} from '../../core.js';
+import {EventDispatcher} from '../../core/event-dispatcher.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
+import {generateId, isObject} from '../../core/utilities.js';
import {OptionsUtil} from '../../data/options-util.js';
import {PermissionsUtil} from '../../data/permissions-util.js';
import {HtmlTemplateCollection} from '../../dom/html-template-collection.js';
diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js
index 13c3d73a..7e458043 100644
--- a/ext/js/pages/settings/settings-main.js
+++ b/ext/js/pages/settings/settings-main.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log} from '../../core.js';
+import {log} from '../../core/logger.js';
import {DocumentFocusController} from '../../dom/document-focus-controller.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
import {yomitan} from '../../yomitan.js';
diff --git a/ext/js/pages/settings/translation-text-replacements-controller.js b/ext/js/pages/settings/translation-text-replacements-controller.js
index 745011b3..98a16026 100644
--- a/ext/js/pages/settings/translation-text-replacements-controller.js
+++ b/ext/js/pages/settings/translation-text-replacements-controller.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {EventListenerCollection} from '../../core.js';
+import {EventListenerCollection} from '../../core/event-listener-collection.js';
import {querySelectorNotNull} from '../../dom/query-selector.js';
export class TranslationTextReplacementsController {
diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js
index 86a57cce..35472ec2 100644
--- a/ext/js/pages/welcome-main.js
+++ b/ext/js/pages/welcome-main.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {log} from '../core.js';
+import {log} from '../core/logger.js';
import {DocumentFocusController} from '../dom/document-focus-controller.js';
import {querySelectorNotNull} from '../dom/query-selector.js';
import {yomitan} from '../yomitan.js';
diff --git a/ext/js/templates/template-renderer-proxy.js b/ext/js/templates/template-renderer-proxy.js
index 0f2abf13..e4814ec4 100644
--- a/ext/js/templates/template-renderer-proxy.js
+++ b/ext/js/templates/template-renderer-proxy.js
@@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {generateId} from '../core.js';
+import {generateId} from '../core/utilities.js';
import {ExtensionError} from '../core/extension-error.js';
export class TemplateRendererProxy {
diff --git a/ext/js/yomitan.js b/ext/js/yomitan.js
index 190392d9..cd3f65fd 100644
--- a/ext/js/yomitan.js
+++ b/ext/js/yomitan.js
@@ -18,9 +18,11 @@
import {API} from './comm/api.js';
import {CrossFrameAPI} from './comm/cross-frame-api.js';
-import {EventDispatcher, deferPromise, log} from './core.js';
import {createApiMap, invokeApiMapHandler} from './core/api-map.js';
+import {EventDispatcher} from './core/event-dispatcher.js';
import {ExtensionError} from './core/extension-error.js';
+import {log} from './core/logger.js';
+import {deferPromise} from './core/utilities.js';
/**
* @returns {boolean}