diff options
81 files changed, 923 insertions, 812 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} diff --git a/test/core.test.js b/test/core.test.js index c7370abf..0ddcc2d1 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -17,7 +17,8 @@   */  import {describe, expect, test} from 'vitest'; -import {DynamicProperty, deepEqual} from '../ext/js/core.js'; +import {DynamicProperty} from '../ext/js/core/dynamic-property.js'; +import {deepEqual} from '../ext/js/core/utilities.js';  /** */  function testDynamicProperty() { |