diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2024-01-31 08:38:30 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-31 13:38:30 +0000 |
commit | 87ed7c8affd3ade9d3cd2d9ed1a61dd5f224e473 (patch) | |
tree | be727294e31ef21e8a3f634734610e69e4a155ac | |
parent | 3e419aa562aab03ca20421aaf7e4d1a39194a5b4 (diff) |
Module refactoring (#588)
* Convert PronunciationGenerator into static functions
* Convert DictionaryDataUtil into static functions
* Convert AnkiNoteDataCreator into static functions
* Convert MediaUtil into static functions
* Convert RegexUtil into static functions
* Convert StringUtil into static functions
* Convert ArrayBufferUtil into static functions
* Convert AnkiUtil into static functions
* Convert PermissionsUtil into static functions
* Convert ProfileConditionsUtil into static functions
38 files changed, 2138 insertions, 2221 deletions
diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 0773dc4b..b95626f5 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -26,19 +26,19 @@ 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 {isNoteDataValid} from '../data/anki-util.js'; import {OptionsUtil} from '../data/options-util.js'; -import {PermissionsUtil} from '../data/permissions-util.js'; -import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; +import {getAllPermissions, hasPermissions, hasRequiredPermissionsForOptions} from '../data/permissions-util.js'; +import {arrayBufferToBase64} from '../data/sandbox/array-buffer-util.js'; import {DictionaryDatabase} from '../dictionary/dictionary-database.js'; import {Environment} from '../extension/environment.js'; import {ObjectPropertyAccessor} from '../general/object-property-accessor.js'; import {distributeFuriganaInflected, isCodePointJapanese, isStringPartiallyJapanese, convertKatakanaToHiragana as jpConvertKatakanaToHiragana} from '../language/japanese.js'; import {Translator} from '../language/translator.js'; import {AudioDownloader} from '../media/audio-downloader.js'; -import {MediaUtil} from '../media/media-util.js'; +import {getFileExtensionFromAudioMediaType, getFileExtensionFromImageMediaType} from '../media/media-util.js'; import {ClipboardReaderProxy, DictionaryDatabaseProxy, OffscreenProxy, TranslatorProxy} from './offscreen-proxy.js'; -import {ProfileConditionsUtil} from './profile-conditions-util.js'; +import {createSchema, normalizeContext} from './profile-conditions-util.js'; import {RequestBuilder} from './request-builder.js'; import {injectStylesheet} from './script-manager.js'; @@ -95,8 +95,6 @@ export class Backend { this._options = null; /** @type {import('../data/json-schema.js').JsonSchema[]} */ this._profileConditionsSchemaCache = []; - /** @type {ProfileConditionsUtil} */ - this._profileConditionsUtil = new ProfileConditionsUtil(); /** @type {?string} */ this._defaultAnkiFieldTemplates = null; /** @type {RequestBuilder} */ @@ -138,8 +136,6 @@ export class Backend { this._logErrorLevel = null; /** @type {?chrome.permissions.Permissions} */ this._permissions = null; - /** @type {PermissionsUtil} */ - this._permissionsUtil = new PermissionsUtil(); /** @type {Map<string, (() => void)[]>} */ this._applicationReadyHandlers = new Map(); @@ -259,7 +255,7 @@ export class Backend { try { this._prepareInternalSync(); - this._permissions = await this._permissionsUtil.getAllPermissions(); + this._permissions = await getAllPermissions(); this._defaultBrowserActionTitle = await this._getBrowserIconTitle(); this._badgePrepareDelayTimer = setTimeout(() => { this._badgePrepareDelayTimer = null; @@ -545,7 +541,7 @@ export class Backend { for (let i = 0; i < notes.length; ++i) { const note = notes[i]; let canAdd = canAddArray[i]; - const valid = AnkiUtil.isNoteDataValid(note); + const valid = isNoteDataValid(note); if (!valid) { canAdd = false; } const info = {canAdd, valid, noteIds: null}; results.push(info); @@ -815,7 +811,7 @@ export class Backend { let permissionsOkay = false; try { - permissionsOkay = await this._permissionsUtil.hasPermissions({permissions: ['nativeMessaging']}); + permissionsOkay = await hasPermissions({permissions: ['nativeMessaging']}); } catch (e) { // NOP } @@ -1302,7 +1298,7 @@ export class Backend { * @returns {?import('settings').Profile} */ _getProfileFromContext(options, optionsContext) { - const normalizedOptionsContext = this._profileConditionsUtil.normalizeContext(optionsContext); + const normalizedOptionsContext = normalizeContext(optionsContext); let index = 0; for (const profile of options.profiles) { @@ -1312,7 +1308,7 @@ export class Backend { if (index < this._profileConditionsSchemaCache.length) { schema = this._profileConditionsSchemaCache[index]; } else { - schema = this._profileConditionsUtil.createSchema(conditionGroups); + schema = createSchema(conditionGroups); this._profileConditionsSchemaCache.push(schema); } @@ -2128,7 +2124,7 @@ export class Backend { return null; } - let extension = contentType !== null ? MediaUtil.getFileExtensionFromAudioMediaType(contentType) : null; + let extension = contentType !== null ? getFileExtensionFromAudioMediaType(contentType) : null; if (extension === null) { extension = '.mp3'; } let fileName = this._generateAnkiNoteMediaFileName('yomitan_audio', extension, timestamp, definitionDetails); fileName = fileName.replace(/\]/g, ''); @@ -2147,7 +2143,7 @@ export class Backend { const dataUrl = await this._getScreenshot(tabId, frameId, format, quality); const {mediaType, data} = this._getDataUrlInfo(dataUrl); - const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); + const extension = getFileExtensionFromImageMediaType(mediaType); if (extension === null) { throw new Error('Unknown media type for screenshot image'); } @@ -2169,7 +2165,7 @@ export class Backend { } const {mediaType, data} = this._getDataUrlInfo(dataUrl); - const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); + const extension = getFileExtensionFromImageMediaType(mediaType); if (extension === null) { throw new Error('Unknown media type for clipboard image'); } @@ -2215,7 +2211,7 @@ export class Backend { let fileName = null; if (media !== null) { const {content, mediaType} = media; - const extension = MediaUtil.getFileExtensionFromImageMediaType(mediaType); + const extension = getFileExtensionFromImageMediaType(mediaType); fileName = this._generateAnkiNoteMediaFileName( `yomitan_dictionary_media_${i + 1}`, extension !== null ? extension : '', @@ -2611,7 +2607,7 @@ export class Backend { * @returns {Promise<void>} */ async _checkPermissions() { - this._permissions = await this._permissionsUtil.getAllPermissions(); + this._permissions = await getAllPermissions(); this._updateBadge(); } @@ -2628,7 +2624,7 @@ export class Backend { */ _hasRequiredPermissionsForSettings(options) { if (!this._canObservePermissionsChanges()) { return true; } - return this._permissions === null || this._permissionsUtil.hasRequiredPermissionsForOptions(this._permissions, options); + return this._permissions === null || hasRequiredPermissionsForOptions(this._permissions, options); } /** @@ -2663,7 +2659,7 @@ export class Backend { const results = []; for (const item of await this._dictionaryDatabase.getMedia(targets)) { const {content, dictionary, height, mediaType, path, width} = item; - const content2 = ArrayBufferUtil.arrayBufferToBase64(content); + const content2 = arrayBufferToBase64(content); results.push({content: content2, dictionary, height, mediaType, path, width}); } return results; diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js index 80ff31c0..716deddd 100644 --- a/ext/js/background/offscreen-proxy.js +++ b/ext/js/background/offscreen-proxy.js @@ -18,7 +18,7 @@ import {ExtensionError} from '../core/extension-error.js'; import {isObject} from '../core/utilities.js'; -import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; +import {base64ToArrayBuffer} from '../data/sandbox/array-buffer-util.js'; export class OffscreenProxy { /** @@ -144,7 +144,7 @@ export class DictionaryDatabaseProxy { */ async getMedia(targets) { const serializedMedia = /** @type {import('dictionary-database').Media<string>[]} */ (await this._offscreen.sendMessagePromise({action: 'databaseGetMediaOffscreen', params: {targets}})); - const media = serializedMedia.map((m) => ({...m, content: ArrayBufferUtil.base64ToArrayBuffer(m.content)})); + const media = serializedMedia.map((m) => ({...m, content: base64ToArrayBuffer(m.content)})); return media; } } diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js index ef05508a..b203e326 100644 --- a/ext/js/background/offscreen.js +++ b/ext/js/background/offscreen.js @@ -18,7 +18,7 @@ import {ClipboardReader} from '../comm/clipboard-reader.js'; import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; -import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; +import {arrayBufferToBase64} from '../data/sandbox/array-buffer-util.js'; import {DictionaryDatabase} from '../dictionary/dictionary-database.js'; import {Translator} from '../language/translator.js'; @@ -110,7 +110,7 @@ export class Offscreen { /** @type {import('offscreen').ApiHandler<'databaseGetMediaOffscreen'>} */ async _getMediaHandler({targets}) { const media = await this._dictionaryDatabase.getMedia(targets); - const serializedMedia = media.map((m) => ({...m, content: ArrayBufferUtil.arrayBufferToBase64(m.content)})); + const serializedMedia = media.map((m) => ({...m, content: arrayBufferToBase64(m.content)})); return serializedMedia; } diff --git a/ext/js/background/profile-conditions-util.js b/ext/js/background/profile-conditions-util.js index f3be226d..e2d58725 100644 --- a/ext/js/background/profile-conditions-util.js +++ b/ext/js/background/profile-conditions-util.js @@ -18,379 +18,369 @@ import {JsonSchema} from '../data/json-schema.js'; +/** @type {RegExp} */ +const splitPattern = /[,;\s]+/; +/** @type {Map<string, {operators: Map<string, import('profile-conditions-util').CreateSchemaFunction>}>} */ +const descriptors = new Map([ + [ + 'popupLevel', + { + operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ + ['equal', createSchemaPopupLevelEqual.bind(this)], + ['notEqual', createSchemaPopupLevelNotEqual.bind(this)], + ['lessThan', createSchemaPopupLevelLessThan.bind(this)], + ['greaterThan', createSchemaPopupLevelGreaterThan.bind(this)], + ['lessThanOrEqual', createSchemaPopupLevelLessThanOrEqual.bind(this)], + ['greaterThanOrEqual', createSchemaPopupLevelGreaterThanOrEqual.bind(this)] + ])) + } + ], + [ + 'url', + { + operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ + ['matchDomain', createSchemaUrlMatchDomain.bind(this)], + ['matchRegExp', createSchemaUrlMatchRegExp.bind(this)] + ])) + } + ], + [ + 'modifierKeys', + { + operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ + ['are', createSchemaModifierKeysAre.bind(this)], + ['areNot', createSchemaModifierKeysAreNot.bind(this)], + ['include', createSchemaModifierKeysInclude.bind(this)], + ['notInclude', createSchemaModifierKeysNotInclude.bind(this)] + ])) + } + ], + [ + 'flags', + { + operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ + ['are', createSchemaFlagsAre.bind(this)], + ['areNot', createSchemaFlagsAreNot.bind(this)], + ['include', createSchemaFlagsInclude.bind(this)], + ['notInclude', createSchemaFlagsNotInclude.bind(this)] + ])) + } + ] +]); + /** - * Utility class to help processing profile conditions. + * Creates a new JSON schema descriptor for the given set of condition groups. + * @param {import('settings').ProfileConditionGroup[]} conditionGroups An array of condition groups. + * For a profile match, all of the items must return successfully in at least one of the groups. + * @returns {JsonSchema} A new `JsonSchema` object. */ -export class ProfileConditionsUtil { - /** - * Creates a new instance. - */ - constructor() { - /** @type {RegExp} */ - this._splitPattern = /[,;\s]+/; - /** @type {Map<string, {operators: Map<string, import('profile-conditions-util').CreateSchemaFunction>}>} */ - this._descriptors = new Map([ - [ - 'popupLevel', - { - operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ - ['equal', this._createSchemaPopupLevelEqual.bind(this)], - ['notEqual', this._createSchemaPopupLevelNotEqual.bind(this)], - ['lessThan', this._createSchemaPopupLevelLessThan.bind(this)], - ['greaterThan', this._createSchemaPopupLevelGreaterThan.bind(this)], - ['lessThanOrEqual', this._createSchemaPopupLevelLessThanOrEqual.bind(this)], - ['greaterThanOrEqual', this._createSchemaPopupLevelGreaterThanOrEqual.bind(this)] - ])) - } - ], - [ - 'url', - { - operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ - ['matchDomain', this._createSchemaUrlMatchDomain.bind(this)], - ['matchRegExp', this._createSchemaUrlMatchRegExp.bind(this)] - ])) - } - ], - [ - 'modifierKeys', - { - operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ - ['are', this._createSchemaModifierKeysAre.bind(this)], - ['areNot', this._createSchemaModifierKeysAreNot.bind(this)], - ['include', this._createSchemaModifierKeysInclude.bind(this)], - ['notInclude', this._createSchemaModifierKeysNotInclude.bind(this)] - ])) - } - ], - [ - 'flags', - { - operators: new Map(/** @type {import('profile-conditions-util').OperatorMapArray} */ ([ - ['are', this._createSchemaFlagsAre.bind(this)], - ['areNot', this._createSchemaFlagsAreNot.bind(this)], - ['include', this._createSchemaFlagsInclude.bind(this)], - ['notInclude', this._createSchemaFlagsNotInclude.bind(this)] - ])) - } - ] - ]); - } +export function createSchema(conditionGroups) { + const anyOf = []; + for (const {conditions} of conditionGroups) { + const allOf = []; + for (const {type, operator, value} of conditions) { + const conditionDescriptor = descriptors.get(type); + if (typeof conditionDescriptor === 'undefined') { continue; } - /** - * Creates a new JSON schema descriptor for the given set of condition groups. - * @param {import('settings').ProfileConditionGroup[]} conditionGroups An array of condition groups. - * For a profile match, all of the items must return successfully in at least one of the groups. - * @returns {JsonSchema} A new `JsonSchema` object. - */ - createSchema(conditionGroups) { - const anyOf = []; - for (const {conditions} of conditionGroups) { - const allOf = []; - for (const {type, operator, value} of conditions) { - const conditionDescriptor = this._descriptors.get(type); - if (typeof conditionDescriptor === 'undefined') { continue; } + const createSchema2 = conditionDescriptor.operators.get(operator); + if (typeof createSchema2 === 'undefined') { continue; } - const createSchema = conditionDescriptor.operators.get(operator); - if (typeof createSchema === 'undefined') { continue; } - - const schema = createSchema(value); - allOf.push(schema); - } - switch (allOf.length) { - case 0: break; - case 1: anyOf.push(allOf[0]); break; - default: anyOf.push({allOf}); break; - } + const schema = createSchema2(value); + allOf.push(schema); } - let schema; - switch (anyOf.length) { - case 0: schema = {}; break; - case 1: schema = anyOf[0]; break; - default: schema = {anyOf}; break; + switch (allOf.length) { + case 0: break; + case 1: anyOf.push(allOf[0]); break; + default: anyOf.push({allOf}); break; } - return new JsonSchema(schema); } + let schema; + switch (anyOf.length) { + case 0: schema = {}; break; + case 1: schema = anyOf[0]; break; + default: schema = {anyOf}; break; + } + return new JsonSchema(schema); +} - /** - * Creates a normalized version of the context object to test, - * assigning dependent fields as needed. - * @param {import('settings').OptionsContext} context A context object which is used during schema validation. - * @returns {import('profile-conditions-util').NormalizedOptionsContext} A normalized context object. - */ - normalizeContext(context) { - const normalizedContext = /** @type {import('profile-conditions-util').NormalizedOptionsContext} */ (Object.assign({}, context)); - const {url} = normalizedContext; - if (typeof url === 'string') { - try { - normalizedContext.domain = new URL(url).hostname; - } catch (e) { - // NOP - } - } - const {flags} = normalizedContext; - if (!Array.isArray(flags)) { - normalizedContext.flags = []; +/** + * Creates a normalized version of the context object to test, + * assigning dependent fields as needed. + * @param {import('settings').OptionsContext} context A context object which is used during schema validation. + * @returns {import('profile-conditions-util').NormalizedOptionsContext} A normalized context object. + */ +export function normalizeContext(context) { + const normalizedContext = /** @type {import('profile-conditions-util').NormalizedOptionsContext} */ (Object.assign({}, context)); + const {url} = normalizedContext; + if (typeof url === 'string') { + try { + normalizedContext.domain = new URL(url).hostname; + } catch (e) { + // NOP } - return normalizedContext; } - - // Private - - /** - * @param {string} value - * @returns {string[]} - */ - _split(value) { - return value.split(this._splitPattern); + const {flags} = normalizedContext; + if (!Array.isArray(flags)) { + normalizedContext.flags = []; } + return normalizedContext; +} - /** - * @param {string} value - * @returns {number} - */ - _stringToNumber(value) { - const number = Number.parseFloat(value); - return Number.isFinite(number) ? number : 0; - } +// Private - // popupLevel schema creation functions +/** + * @param {string} value + * @returns {string[]} + */ +function split(value) { + return value.split(splitPattern); +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaPopupLevelEqual(value) { - const number = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {const: number} - } - }; - } +/** + * @param {string} value + * @returns {number} + */ +function stringToNumber(value) { + const number = Number.parseFloat(value); + return Number.isFinite(number) ? number : 0; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaPopupLevelNotEqual(value) { - return { - not: { - anyOf: [this._createSchemaPopupLevelEqual(value)] - } - }; - } +// popupLevel schema creation functions - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaPopupLevelLessThan(value) { - const number = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {type: 'number', exclusiveMaximum: number} - } - }; - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaPopupLevelEqual(value) { + const number = stringToNumber(value); + return { + required: ['depth'], + properties: { + depth: {const: number} + } + }; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaPopupLevelGreaterThan(value) { - const number = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {type: 'number', exclusiveMinimum: number} - } - }; - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaPopupLevelNotEqual(value) { + return { + not: { + anyOf: [createSchemaPopupLevelEqual(value)] + } + }; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaPopupLevelLessThanOrEqual(value) { - const number = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {type: 'number', maximum: number} - } - }; - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaPopupLevelLessThan(value) { + const number = stringToNumber(value); + return { + required: ['depth'], + properties: { + depth: {type: 'number', exclusiveMaximum: number} + } + }; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaPopupLevelGreaterThanOrEqual(value) { - const number = this._stringToNumber(value); - return { - required: ['depth'], - properties: { - depth: {type: 'number', minimum: number} - } - }; - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaPopupLevelGreaterThan(value) { + const number = stringToNumber(value); + return { + required: ['depth'], + properties: { + depth: {type: 'number', exclusiveMinimum: number} + } + }; +} - // url schema creation functions +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaPopupLevelLessThanOrEqual(value) { + const number = stringToNumber(value); + return { + required: ['depth'], + properties: { + depth: {type: 'number', maximum: number} + } + }; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaUrlMatchDomain(value) { - const oneOf = []; - for (let domain of this._split(value)) { - if (domain.length === 0) { continue; } - domain = domain.toLowerCase(); - oneOf.push({const: domain}); +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaPopupLevelGreaterThanOrEqual(value) { + const number = stringToNumber(value); + return { + required: ['depth'], + properties: { + depth: {type: 'number', minimum: number} } - return { - required: ['domain'], - properties: { - domain: {oneOf} - } - }; - } + }; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaUrlMatchRegExp(value) { - return { - required: ['url'], - properties: { - url: {type: 'string', pattern: value, patternFlags: 'i'} - } - }; +// url schema creation functions + +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaUrlMatchDomain(value) { + const oneOf = []; + for (let domain of split(value)) { + if (domain.length === 0) { continue; } + domain = domain.toLowerCase(); + oneOf.push({const: domain}); } + return { + required: ['domain'], + properties: { + domain: {oneOf} + } + }; +} - // modifierKeys schema creation functions +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaUrlMatchRegExp(value) { + return { + required: ['url'], + properties: { + url: {type: 'string', pattern: value, patternFlags: 'i'} + } + }; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaModifierKeysAre(value) { - return this._createSchemaArrayCheck('modifierKeys', value, true, false); - } +// modifierKeys schema creation functions - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaModifierKeysAreNot(value) { - return { - not: { - anyOf: [this._createSchemaArrayCheck('modifierKeys', value, true, false)] - } - }; - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaModifierKeysAre(value) { + return createSchemaArrayCheck('modifierKeys', value, true, false); +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaModifierKeysInclude(value) { - return this._createSchemaArrayCheck('modifierKeys', value, false, false); - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaModifierKeysAreNot(value) { + return { + not: { + anyOf: [createSchemaArrayCheck('modifierKeys', value, true, false)] + } + }; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaModifierKeysNotInclude(value) { - return this._createSchemaArrayCheck('modifierKeys', value, false, true); - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaModifierKeysInclude(value) { + return createSchemaArrayCheck('modifierKeys', value, false, false); +} - // modifierKeys schema creation functions +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaModifierKeysNotInclude(value) { + return createSchemaArrayCheck('modifierKeys', value, false, true); +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaFlagsAre(value) { - return this._createSchemaArrayCheck('flags', value, true, false); - } +// modifierKeys schema creation functions - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaFlagsAreNot(value) { - return { - not: { - anyOf: [this._createSchemaArrayCheck('flags', value, true, false)] - } - }; - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaFlagsAre(value) { + return createSchemaArrayCheck('flags', value, true, false); +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaFlagsInclude(value) { - return this._createSchemaArrayCheck('flags', value, false, false); - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaFlagsAreNot(value) { + return { + not: { + anyOf: [createSchemaArrayCheck('flags', value, true, false)] + } + }; +} - /** - * @param {string} value - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaFlagsNotInclude(value) { - return this._createSchemaArrayCheck('flags', value, false, true); - } +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaFlagsInclude(value) { + return createSchemaArrayCheck('flags', value, false, false); +} - // Generic +/** + * @param {string} value + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaFlagsNotInclude(value) { + return createSchemaArrayCheck('flags', value, false, true); +} - /** - * @param {string} key - * @param {string} value - * @param {boolean} exact - * @param {boolean} none - * @returns {import('ext/json-schema').Schema} - */ - _createSchemaArrayCheck(key, value, exact, none) { - /** @type {import('ext/json-schema').Schema[]} */ - const containsList = []; - for (const item of this._split(value)) { - if (item.length === 0) { continue; } - containsList.push({ - contains: { - const: item - } - }); - } - const containsListCount = containsList.length; - /** @type {import('ext/json-schema').Schema} */ - const schema = { - type: 'array' - }; - if (exact) { - schema.maxItems = containsListCount; - } - if (none) { - if (containsListCount > 0) { - schema.not = {anyOf: containsList}; - } - } else { - schema.minItems = containsListCount; - if (containsListCount > 0) { - schema.allOf = containsList; +// Generic + +/** + * @param {string} key + * @param {string} value + * @param {boolean} exact + * @param {boolean} none + * @returns {import('ext/json-schema').Schema} + */ +function createSchemaArrayCheck(key, value, exact, none) { + /** @type {import('ext/json-schema').Schema[]} */ + const containsList = []; + for (const item of split(value)) { + if (item.length === 0) { continue; } + containsList.push({ + contains: { + const: item } + }); + } + const containsListCount = containsList.length; + /** @type {import('ext/json-schema').Schema} */ + const schema = { + type: 'array' + }; + if (exact) { + schema.maxItems = containsListCount; + } + if (none) { + if (containsListCount > 0) { + schema.not = {anyOf: containsList}; + } + } else { + schema.minItems = containsListCount; + if (containsListCount > 0) { + schema.allOf = containsList; } - return { - required: [key], - properties: { - [key]: schema - } - }; } + return { + required: [key], + properties: { + [key]: schema + } + }; } diff --git a/ext/js/comm/anki-connect.js b/ext/js/comm/anki-connect.js index f16471ce..7cb2d071 100644 --- a/ext/js/comm/anki-connect.js +++ b/ext/js/comm/anki-connect.js @@ -18,7 +18,7 @@ import {ExtensionError} from '../core/extension-error.js'; import {parseJson} from '../core/json.js'; -import {AnkiUtil} from '../data/anki-util.js'; +import {getRootDeckName} from '../data/anki-util.js'; /** * This class controls communication with Anki via the AnkiConnect plugin. @@ -499,7 +499,7 @@ export class AnkiConnect { query = `"deck:${this._escapeQuery(note.deckName)}" `; break; case 'deck-root': - query = `"deck:${this._escapeQuery(AnkiUtil.getRootDeckName(note.deckName))}" `; + query = `"deck:${this._escapeQuery(getRootDeckName(note.deckName))}" `; break; } query += this._fieldsToQuery(note.fields); diff --git a/ext/js/comm/clipboard-reader.js b/ext/js/comm/clipboard-reader.js index 2ac41cb9..b040d6ca 100644 --- a/ext/js/comm/clipboard-reader.js +++ b/ext/js/comm/clipboard-reader.js @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {MediaUtil} from '../media/media-util.js'; +import {getFileExtensionFromImageMediaType} from '../media/media-util.js'; /** * Class which can read text and images from the clipboard. @@ -130,7 +130,7 @@ export class ClipboardReader { for (const item of items) { for (const type of item.types) { - if (!MediaUtil.getFileExtensionFromImageMediaType(type)) { continue; } + if (!getFileExtensionFromImageMediaType(type)) { continue; } try { const blob = await item.getType(type); return await this._readFileAsDataURL(blob); diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index 815e7f3f..5bb943c2 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -20,7 +20,7 @@ import {ExtensionError} from '../core/extension-error.js'; import {deferPromise} from '../core/utilities.js'; import {convertHiraganaToKatakana, convertKatakanaToHiragana} from '../language/japanese.js'; import {yomitan} from '../yomitan.js'; -import {AnkiUtil} from './anki-util.js'; +import {cloneFieldMarkerPattern, getRootDeckName} from './anki-util.js'; export class AnkiNoteBuilder { /** @@ -29,7 +29,7 @@ export class AnkiNoteBuilder { */ constructor(templateRenderer) { /** @type {RegExp} */ - this._markerPattern = AnkiUtil.cloneFieldMarkerPattern(true); + this._markerPattern = cloneFieldMarkerPattern(true); /** @type {import('../templates/template-renderer-proxy.js').TemplateRendererProxy|import('../templates/sandbox/template-renderer.js').TemplateRenderer} */ this._templateRenderer = templateRenderer; /** @type {import('anki-note-builder').BatchedRequestGroup[]} */ @@ -64,7 +64,7 @@ export class AnkiNoteBuilder { let duplicateScopeCheckChildren = false; if (duplicateScope === 'deck-root') { duplicateScope = 'deck'; - duplicateScopeDeckName = AnkiUtil.getRootDeckName(deckName); + duplicateScopeDeckName = getRootDeckName(deckName); duplicateScopeCheckChildren = true; } diff --git a/ext/js/data/anki-util.js b/ext/js/data/anki-util.js index 57684887..123e5d2f 100644 --- a/ext/js/data/anki-util.js +++ b/ext/js/data/anki-util.js @@ -18,72 +18,67 @@ import {isObject} from '../core/utilities.js'; +/** @type {RegExp} @readonly */ +const markerPattern = /\{([\w-]+)\}/g; + /** - * This class has some general utility functions for working with Anki data. + * Gets the root deck name of a full deck name. If the deck is a root deck, + * the same name is returned. Nested decks are separated using '::'. + * @param {string} deckName A string of the deck name. + * @returns {string} A string corresponding to the name of the root deck. */ -export class AnkiUtil { - /** @type {RegExp} @readonly */ - static _markerPattern = /\{([\w-]+)\}/g; - - /** - * Gets the root deck name of a full deck name. If the deck is a root deck, - * the same name is returned. Nested decks are separated using '::'. - * @param {string} deckName A string of the deck name. - * @returns {string} A string corresponding to the name of the root deck. - */ - static getRootDeckName(deckName) { - const index = deckName.indexOf('::'); - return index >= 0 ? deckName.substring(0, index) : deckName; - } +export function getRootDeckName(deckName) { + const index = deckName.indexOf('::'); + return index >= 0 ? deckName.substring(0, index) : deckName; +} - /** - * Checks whether or not any marker is contained in a string. - * @param {string} string A string to check. - * @returns {boolean} `true` if the text contains an Anki field marker, `false` otherwise. - */ - static stringContainsAnyFieldMarker(string) { - const result = this._markerPattern.test(string); - this._markerPattern.lastIndex = 0; - return result; - } +/** + * Checks whether or not any marker is contained in a string. + * @param {string} string A string to check. + * @returns {boolean} `true` if the text contains an Anki field marker, `false` otherwise. + */ +export function stringContainsAnyFieldMarker(string) { + const result = markerPattern.test(string); + markerPattern.lastIndex = 0; + return result; +} - /** - * Gets a list of all markers that are contained in a string. - * @param {string} string A string to check. - * @returns {string[]} An array of marker strings. - */ - static getFieldMarkers(string) { - const pattern = this._markerPattern; - const markers = []; - while (true) { - const match = pattern.exec(string); - if (match === null) { break; } - markers.push(match[1]); - } - return markers; +/** + * Gets a list of all markers that are contained in a string. + * @param {string} string A string to check. + * @returns {string[]} An array of marker strings. + */ +export function getFieldMarkers(string) { + const pattern = markerPattern; + const markers = []; + while (true) { + const match = pattern.exec(string); + if (match === null) { break; } + markers.push(match[1]); } + return markers; +} - /** - * Returns a regular expression which can be used to find markers in a string. - * @param {boolean} global Whether or not the regular expression should have the global flag. - * @returns {RegExp} A new `RegExp` instance. - */ - static cloneFieldMarkerPattern(global) { - return new RegExp(this._markerPattern.source, global ? 'g' : ''); - } +/** + * Returns a regular expression which can be used to find markers in a string. + * @param {boolean} global Whether or not the regular expression should have the global flag. + * @returns {RegExp} A new `RegExp` instance. + */ +export function cloneFieldMarkerPattern(global) { + return new RegExp(markerPattern.source, global ? 'g' : ''); +} - /** - * Checks whether or not a note object is valid. - * @param {import('anki').Note} note A note object to check. - * @returns {boolean} `true` if the note is valid, `false` otherwise. - */ - static isNoteDataValid(note) { - if (!isObject(note)) { return false; } - const {fields, deckName, modelName} = note; - return ( - typeof deckName === 'string' && - typeof modelName === 'string' && - Object.entries(fields).length > 0 - ); - } +/** + * Checks whether or not a note object is valid. + * @param {import('anki').Note} note A note object to check. + * @returns {boolean} `true` if the note is valid, `false` otherwise. + */ +export function isNoteDataValid(note) { + if (!isObject(note)) { return false; } + const {fields, deckName, modelName} = note; + return ( + typeof deckName === 'string' && + typeof modelName === 'string' && + Object.entries(fields).length > 0 + ); } diff --git a/ext/js/data/permissions-util.js b/ext/js/data/permissions-util.js index 43aaa87c..837b6d5f 100644 --- a/ext/js/data/permissions-util.js +++ b/ext/js/data/permissions-util.js @@ -16,123 +16,128 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {AnkiUtil} from './anki-util.js'; +import {getFieldMarkers} from './anki-util.js'; -export class PermissionsUtil { - constructor() { - /** @type {Set<string>} */ - this._ankiFieldMarkersRequiringClipboardPermission = new Set([ - 'clipboard-image', - 'clipboard-text' - ]); +/** + * This function returns whether an Anki field marker might require clipboard permissions. + * This is speculative and may not guarantee that the field marker actually does require the permission, + * as the custom handlebars template is not deeply inspected. + * @param {string} marker + * @returns {boolean} + */ +function ankiFieldMarkerMayUseClipboard(marker) { + switch (marker) { + case 'clipboard-image': + case 'clipboard-text': + return true; + default: + return false; } +} + +/** + * @param {chrome.permissions.Permissions} permissions + * @returns {Promise<boolean>} + */ +export function hasPermissions(permissions) { + return new Promise((resolve, reject) => chrome.permissions.contains(permissions, (result) => { + const e = chrome.runtime.lastError; + if (e) { + reject(new Error(e.message)); + } else { + resolve(result); + } + })); +} - /** - * @param {chrome.permissions.Permissions} permissions - * @returns {Promise<boolean>} - */ - hasPermissions(permissions) { - return new Promise((resolve, reject) => chrome.permissions.contains(permissions, (result) => { +/** + * @param {chrome.permissions.Permissions} permissions + * @param {boolean} shouldHave + * @returns {Promise<boolean>} + */ +export function setPermissionsGranted(permissions, shouldHave) { + return ( + shouldHave ? + new Promise((resolve, reject) => chrome.permissions.request(permissions, (result) => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); } else { resolve(result); } - })); - } - - /** - * @param {chrome.permissions.Permissions} permissions - * @param {boolean} shouldHave - * @returns {Promise<boolean>} - */ - setPermissionsGranted(permissions, shouldHave) { - return ( - shouldHave ? - new Promise((resolve, reject) => chrome.permissions.request(permissions, (result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(result); - } - })) : - new Promise((resolve, reject) => chrome.permissions.remove(permissions, (result) => { - const e = chrome.runtime.lastError; - if (e) { - reject(new Error(e.message)); - } else { - resolve(!result); - } - })) - ); - } - - /** - * @returns {Promise<chrome.permissions.Permissions>} - */ - getAllPermissions() { - return new Promise((resolve, reject) => chrome.permissions.getAll((result) => { + })) : + new Promise((resolve, reject) => chrome.permissions.remove(permissions, (result) => { const e = chrome.runtime.lastError; if (e) { reject(new Error(e.message)); } else { - resolve(result); + resolve(!result); } - })); - } + })) + ); +} - /** - * @param {string} fieldValue - * @returns {string[]} - */ - getRequiredPermissionsForAnkiFieldValue(fieldValue) { - const markers = AnkiUtil.getFieldMarkers(fieldValue); - const markerPermissions = this._ankiFieldMarkersRequiringClipboardPermission; - for (const marker of markers) { - if (markerPermissions.has(marker)) { - return ['clipboardRead']; - } +/** + * @returns {Promise<chrome.permissions.Permissions>} + */ +export function getAllPermissions() { + return new Promise((resolve, reject) => chrome.permissions.getAll((result) => { + const e = chrome.runtime.lastError; + if (e) { + reject(new Error(e.message)); + } else { + resolve(result); + } + })); +} + +/** + * @param {string} fieldValue + * @returns {string[]} + */ +export function getRequiredPermissionsForAnkiFieldValue(fieldValue) { + const markers = getFieldMarkers(fieldValue); + for (const marker of markers) { + if (ankiFieldMarkerMayUseClipboard(marker)) { + return ['clipboardRead']; } - return []; } + return []; +} - /** - * @param {chrome.permissions.Permissions} permissions - * @param {import('settings').ProfileOptions} options - * @returns {boolean} - */ - hasRequiredPermissionsForOptions(permissions, options) { - const permissionsSet = new Set(permissions.permissions); +/** + * @param {chrome.permissions.Permissions} permissions + * @param {import('settings').ProfileOptions} options + * @returns {boolean} + */ +export function hasRequiredPermissionsForOptions(permissions, options) { + const permissionsSet = new Set(permissions.permissions); - if (!permissionsSet.has('nativeMessaging')) { - if (options.parsing.enableMecabParser) { - return false; - } + if (!permissionsSet.has('nativeMessaging')) { + if (options.parsing.enableMecabParser) { + return false; } + } - if (!permissionsSet.has('clipboardRead')) { - if (options.clipboard.enableBackgroundMonitor || options.clipboard.enableSearchPageMonitor) { - return false; - } - const fieldMarkersRequiringClipboardPermission = this._ankiFieldMarkersRequiringClipboardPermission; - const fieldsList = [ - options.anki.terms.fields, - options.anki.kanji.fields - ]; - for (const fields of fieldsList) { - for (const fieldValue of Object.values(fields)) { - const markers = AnkiUtil.getFieldMarkers(fieldValue); - for (const marker of markers) { - if (fieldMarkersRequiringClipboardPermission.has(marker)) { - return false; - } + if (!permissionsSet.has('clipboardRead')) { + if (options.clipboard.enableBackgroundMonitor || options.clipboard.enableSearchPageMonitor) { + return false; + } + const fieldsList = [ + options.anki.terms.fields, + options.anki.kanji.fields + ]; + for (const fields of fieldsList) { + for (const fieldValue of Object.values(fields)) { + const markers = getFieldMarkers(fieldValue); + for (const marker of markers) { + if (ankiFieldMarkerMayUseClipboard(marker)) { + return false; } } } } - - return true; } + + return true; } diff --git a/ext/js/data/sandbox/anki-note-data-creator.js b/ext/js/data/sandbox/anki-note-data-creator.js index fc787a66..51679662 100644 --- a/ext/js/data/sandbox/anki-note-data-creator.js +++ b/ext/js/data/sandbox/anki-note-data-creator.js @@ -16,867 +16,842 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {DictionaryDataUtil} from '../../dictionary/dictionary-data-util.js'; +import {getDisambiguations, getGroupedPronunciations, getPronunciationsOfType, getTermFrequency, groupTermTags} from '../../dictionary/dictionary-data-util.js'; import {distributeFurigana} from '../../language/japanese.js'; /** - * This class is used to convert the internal dictionary entry format to the - * format used by Anki, for backwards compatibility. - */ -export class AnkiNoteDataCreator { - /** - * Creates a compatibility representation of the specified data. - * @param {string} marker The marker that is being used for template rendering. - * @param {import('anki-templates-internal').CreateDetails} details Information which is used to generate the data. - * @returns {import('anki-templates').NoteData} An object used for rendering Anki templates. - */ - create(marker, { - dictionaryEntry, - resultOutputMode, - mode, - glossaryLayoutMode, - compactTags, - context, - media - }) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - const definition = this.createCachedValue(this._getDefinition.bind(this, dictionaryEntry, context, resultOutputMode)); - const uniqueExpressions = this.createCachedValue(this._getUniqueExpressions.bind(this, dictionaryEntry)); - const uniqueReadings = this.createCachedValue(this._getUniqueReadings.bind(this, dictionaryEntry)); - const context2 = this.createCachedValue(this._getPublicContext.bind(this, context)); - const pitches = this.createCachedValue(this._getPitches.bind(this, dictionaryEntry)); - const pitchCount = this.createCachedValue(this._getPitchCount.bind(this, pitches)); - const phoneticTranscriptions = this.createCachedValue(this._getPhoneticTranscriptions.bind(this, dictionaryEntry)); - - if (typeof media !== 'object' || media === null || Array.isArray(media)) { - media = { - audio: void 0, - screenshot: void 0, - clipboardImage: void 0, - clipboardText: void 0, - selectionText: void 0, - textFurigana: [], - dictionaryMedia: {} - }; - } - /** @type {import('anki-templates').NoteData} */ - const result = { - marker, - get definition() { return self.getCachedValue(definition); }, - glossaryLayoutMode, - compactTags, - group: (resultOutputMode === 'group'), - merge: (resultOutputMode === 'merge'), - modeTermKanji: (mode === 'term-kanji'), - modeTermKana: (mode === 'term-kana'), - modeKanji: (mode === 'kanji'), - compactGlossaries: (glossaryLayoutMode === 'compact'), - get uniqueExpressions() { return self.getCachedValue(uniqueExpressions); }, - get uniqueReadings() { return self.getCachedValue(uniqueReadings); }, - get pitches() { return self.getCachedValue(pitches); }, - get pitchCount() { return self.getCachedValue(pitchCount); }, - get phoneticTranscriptions() { return self.getCachedValue(phoneticTranscriptions); }, - get context() { return self.getCachedValue(context2); }, - media, - dictionaryEntry + * Creates a compatibility representation of the specified data. + * @param {string} marker The marker that is being used for template rendering. + * @param {import('anki-templates-internal').CreateDetails} details Information which is used to generate the data. + * @returns {import('anki-templates').NoteData} An object used for rendering Anki templates. + */ +export function createAnkiNoteData(marker, { + dictionaryEntry, + resultOutputMode, + mode, + glossaryLayoutMode, + compactTags, + context, + media +}) { + const definition = createCachedValue(getDefinition.bind(null, dictionaryEntry, context, resultOutputMode)); + const uniqueExpressions = createCachedValue(getUniqueExpressions.bind(null, dictionaryEntry)); + const uniqueReadings = createCachedValue(getUniqueReadings.bind(null, dictionaryEntry)); + const context2 = createCachedValue(getPublicContext.bind(null, context)); + const pitches = createCachedValue(getPitches.bind(null, dictionaryEntry)); + const pitchCount = createCachedValue(getPitchCount.bind(null, pitches)); + const phoneticTranscriptions = createCachedValue(getPhoneticTranscriptions.bind(null, dictionaryEntry)); + + if (typeof media !== 'object' || media === null || Array.isArray(media)) { + media = { + audio: void 0, + screenshot: void 0, + clipboardImage: void 0, + clipboardText: void 0, + selectionText: void 0, + textFurigana: [], + dictionaryMedia: {} }; - Object.defineProperty(result, 'dictionaryEntry', { - configurable: false, - enumerable: false, - writable: false, - value: dictionaryEntry - }); - return result; } + /** @type {import('anki-templates').NoteData} */ + const result = { + marker, + get definition() { return getCachedValue(definition); }, + glossaryLayoutMode, + compactTags, + group: (resultOutputMode === 'group'), + merge: (resultOutputMode === 'merge'), + modeTermKanji: (mode === 'term-kanji'), + modeTermKana: (mode === 'term-kana'), + modeKanji: (mode === 'kanji'), + compactGlossaries: (glossaryLayoutMode === 'compact'), + get uniqueExpressions() { return getCachedValue(uniqueExpressions); }, + get uniqueReadings() { return getCachedValue(uniqueReadings); }, + get pitches() { return getCachedValue(pitches); }, + get pitchCount() { return getCachedValue(pitchCount); }, + get phoneticTranscriptions() { return getCachedValue(phoneticTranscriptions); }, + get context() { return getCachedValue(context2); }, + media, + dictionaryEntry + }; + Object.defineProperty(result, 'dictionaryEntry', { + configurable: false, + enumerable: false, + writable: false, + value: dictionaryEntry + }); + return result; +} - /** - * Creates a deferred-evaluation value. - * @template [T=unknown] - * @param {() => T} getter The function to invoke to get the return value. - * @returns {import('anki-templates-internal').CachedValue<T>} An object which can be passed into `getCachedValue`. - */ - createCachedValue(getter) { - return {getter, hasValue: false, value: void 0}; - } +/** + * Creates a deferred-evaluation value. + * @template [T=unknown] + * @param {() => T} getter The function to invoke to get the return value. + * @returns {import('anki-templates-internal').CachedValue<T>} An object which can be passed into `getCachedValue`. + */ +export function createCachedValue(getter) { + return {getter, hasValue: false, value: void 0}; +} - /** - * Gets the value of a cached object. - * @template [T=unknown] - * @param {import('anki-templates-internal').CachedValue<T>} item An object that was returned from `createCachedValue`. - * @returns {T} The result of evaluating the getter, which is cached after the first invocation. - */ - getCachedValue(item) { - if (item.hasValue) { return /** @type {T} */ (item.value); } - const value = item.getter(); - item.value = value; - item.hasValue = true; - return value; - } +/** + * Gets the value of a cached object. + * @template [T=unknown] + * @param {import('anki-templates-internal').CachedValue<T>} item An object that was returned from `createCachedValue`. + * @returns {T} The result of evaluating the getter, which is cached after the first invocation. + */ +export function getCachedValue(item) { + if (item.hasValue) { return /** @type {T} */ (item.value); } + const value = item.getter(); + item.value = value; + item.hasValue = true; + return value; +} - // Private +// Private - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {?import('dictionary').TermSource} - */ - _getPrimarySource(dictionaryEntry) { - for (const headword of dictionaryEntry.headwords) { - for (const source of headword.sources) { - if (source.isPrimary) { return source; } - } +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {?import('dictionary').TermSource} + */ +function getPrimarySource(dictionaryEntry) { + for (const headword of dictionaryEntry.headwords) { + for (const source of headword.sources) { + if (source.isPrimary) { return source; } } - return null; } + return null; +} - /** - * @param {import('dictionary').DictionaryEntry} dictionaryEntry - * @returns {string[]} - */ - _getUniqueExpressions(dictionaryEntry) { - if (dictionaryEntry.type === 'term') { - const results = new Set(); - for (const {term} of dictionaryEntry.headwords) { - results.add(term); - } - return [...results]; - } else { - return []; +/** + * @param {import('dictionary').DictionaryEntry} dictionaryEntry + * @returns {string[]} + */ +function getUniqueExpressions(dictionaryEntry) { + if (dictionaryEntry.type === 'term') { + const results = new Set(); + for (const {term} of dictionaryEntry.headwords) { + results.add(term); } + return [...results]; + } else { + return []; } +} - /** - * @param {import('dictionary').DictionaryEntry} dictionaryEntry - * @returns {string[]} - */ - _getUniqueReadings(dictionaryEntry) { - if (dictionaryEntry.type === 'term') { - const results = new Set(); - for (const {reading} of dictionaryEntry.headwords) { - results.add(reading); - } - return [...results]; - } else { - return []; +/** + * @param {import('dictionary').DictionaryEntry} dictionaryEntry + * @returns {string[]} + */ +function getUniqueReadings(dictionaryEntry) { + if (dictionaryEntry.type === 'term') { + const results = new Set(); + for (const {reading} of dictionaryEntry.headwords) { + results.add(reading); } + return [...results]; + } else { + return []; } +} - /** - * @param {import('anki-templates-internal').Context} context - * @returns {import('anki-templates').Context} - */ - _getPublicContext(context) { - let {documentTitle, query, fullQuery} = context; - if (typeof documentTitle !== 'string') { documentTitle = ''; } - return { - query, - fullQuery, - document: { - title: documentTitle - } - }; - } +/** + * @param {import('anki-templates-internal').Context} context + * @returns {import('anki-templates').Context} + */ +function getPublicContext(context) { + let {documentTitle, query, fullQuery} = context; + if (typeof documentTitle !== 'string') { documentTitle = ''; } + return { + query, + fullQuery, + document: { + title: documentTitle + } + }; +} - /** - * @param {import('dictionary').DictionaryEntry} dictionaryEntry - * @returns {import('anki-templates').PitchGroup[]} - */ - _getPitches(dictionaryEntry) { - /** @type {import('anki-templates').PitchGroup[]} */ - const results = []; - if (dictionaryEntry.type === 'term') { - for (const {dictionary, pronunciations} of DictionaryDataUtil.getGroupedPronunciations(dictionaryEntry)) { - /** @type {import('anki-templates').Pitch[]} */ - const pitches = []; - for (const groupedPronunciation of pronunciations) { - const {pronunciation} = groupedPronunciation; - if (pronunciation.type !== 'pitch-accent') { continue; } - const {position, nasalPositions, devoicePositions, tags} = pronunciation; - const {terms, reading, exclusiveTerms, exclusiveReadings} = groupedPronunciation; - pitches.push({ - expressions: terms, - reading, - position, - nasalPositions, - devoicePositions, - tags: this._convertPitchTags(tags), - exclusiveExpressions: exclusiveTerms, - exclusiveReadings - }); - } - results.push({dictionary, pitches}); +/** + * @param {import('dictionary').DictionaryEntry} dictionaryEntry + * @returns {import('anki-templates').PitchGroup[]} + */ +function getPitches(dictionaryEntry) { + /** @type {import('anki-templates').PitchGroup[]} */ + const results = []; + if (dictionaryEntry.type === 'term') { + for (const {dictionary, pronunciations} of getGroupedPronunciations(dictionaryEntry)) { + /** @type {import('anki-templates').Pitch[]} */ + const pitches = []; + for (const groupedPronunciation of pronunciations) { + const {pronunciation} = groupedPronunciation; + if (pronunciation.type !== 'pitch-accent') { continue; } + const {position, nasalPositions, devoicePositions, tags} = pronunciation; + const {terms, reading, exclusiveTerms, exclusiveReadings} = groupedPronunciation; + pitches.push({ + expressions: terms, + reading, + position, + nasalPositions, + devoicePositions, + tags: convertPitchTags(tags), + exclusiveExpressions: exclusiveTerms, + exclusiveReadings + }); } + results.push({dictionary, pitches}); } - return results; } + return results; +} - /** - * @param {import('dictionary').DictionaryEntry} dictionaryEntry - * @returns {import('anki-templates').TranscriptionGroup[]} - */ - _getPhoneticTranscriptions(dictionaryEntry) { - const results = []; - if (dictionaryEntry.type === 'term') { - for (const {dictionary, pronunciations} of DictionaryDataUtil.getGroupedPronunciations(dictionaryEntry)) { - const phoneticTranscriptions = []; - for (const groupedPronunciation of pronunciations) { - const {pronunciation} = groupedPronunciation; - if (pronunciation.type !== 'phonetic-transcription') { continue; } - const {ipa, tags} = pronunciation; - const {terms, reading, exclusiveTerms, exclusiveReadings} = groupedPronunciation; - phoneticTranscriptions.push({ - expressions: terms, - reading, - ipa, - tags, - exclusiveExpressions: exclusiveTerms, - exclusiveReadings - }); - } - results.push({dictionary, phoneticTranscriptions}); +/** + * @param {import('dictionary').DictionaryEntry} dictionaryEntry + * @returns {import('anki-templates').TranscriptionGroup[]} + */ +function getPhoneticTranscriptions(dictionaryEntry) { + const results = []; + if (dictionaryEntry.type === 'term') { + for (const {dictionary, pronunciations} of getGroupedPronunciations(dictionaryEntry)) { + const phoneticTranscriptions = []; + for (const groupedPronunciation of pronunciations) { + const {pronunciation} = groupedPronunciation; + if (pronunciation.type !== 'phonetic-transcription') { continue; } + const {ipa, tags} = pronunciation; + const {terms, reading, exclusiveTerms, exclusiveReadings} = groupedPronunciation; + phoneticTranscriptions.push({ + expressions: terms, + reading, + ipa, + tags, + exclusiveExpressions: exclusiveTerms, + exclusiveReadings + }); } + results.push({dictionary, phoneticTranscriptions}); } - return results; } + return results; +} - /** - * @param {import('anki-templates-internal').CachedValue<import('anki-templates').PitchGroup[]>} cachedPitches - * @returns {number} - */ - _getPitchCount(cachedPitches) { - const pitches = this.getCachedValue(cachedPitches); - return pitches.reduce((i, v) => i + v.pitches.length, 0); - } +/** + * @param {import('anki-templates-internal').CachedValue<import('anki-templates').PitchGroup[]>} cachedPitches + * @returns {number} + */ +function getPitchCount(cachedPitches) { + const pitches = getCachedValue(cachedPitches); + return pitches.reduce((i, v) => i + v.pitches.length, 0); +} - /** - * @param {import('dictionary').DictionaryEntry} dictionaryEntry - * @param {import('anki-templates-internal').Context} context - * @param {import('settings').ResultOutputMode} resultOutputMode - * @returns {import('anki-templates').DictionaryEntry} - */ - _getDefinition(dictionaryEntry, context, resultOutputMode) { - switch (dictionaryEntry.type) { - case 'term': - return this._getTermDefinition(dictionaryEntry, context, resultOutputMode); - case 'kanji': - return this._getKanjiDefinition(dictionaryEntry, context); - default: - return /** @type {import('anki-templates').UnknownDictionaryEntry} */ ({}); - } +/** + * @param {import('dictionary').DictionaryEntry} dictionaryEntry + * @param {import('anki-templates-internal').Context} context + * @param {import('settings').ResultOutputMode} resultOutputMode + * @returns {import('anki-templates').DictionaryEntry} + */ +function getDefinition(dictionaryEntry, context, resultOutputMode) { + switch (dictionaryEntry.type) { + case 'term': + return getTermDefinition(dictionaryEntry, context, resultOutputMode); + case 'kanji': + return getKanjiDefinition(dictionaryEntry, context); + default: + return /** @type {import('anki-templates').UnknownDictionaryEntry} */ ({}); } +} - /** - * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry - * @param {import('anki-templates-internal').Context} context - * @returns {import('anki-templates').KanjiDictionaryEntry} - */ - _getKanjiDefinition(dictionaryEntry, context) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - - const {character, dictionary, onyomi, kunyomi, definitions} = dictionaryEntry; +/** + * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry + * @param {import('anki-templates-internal').Context} context + * @returns {import('anki-templates').KanjiDictionaryEntry} + */ +function getKanjiDefinition(dictionaryEntry, context) { + const {character, dictionary, onyomi, kunyomi, definitions} = dictionaryEntry; + + let {url} = context; + if (typeof url !== 'string') { url = ''; } + + const stats = createCachedValue(getKanjiStats.bind(null, dictionaryEntry)); + const tags = createCachedValue(convertTags.bind(null, dictionaryEntry.tags)); + const frequencies = createCachedValue(getKanjiFrequencies.bind(null, dictionaryEntry)); + const cloze = createCachedValue(getCloze.bind(null, dictionaryEntry, context)); + + return { + type: 'kanji', + character, + dictionary, + onyomi, + kunyomi, + glossary: definitions, + get tags() { return getCachedValue(tags); }, + get stats() { return getCachedValue(stats); }, + get frequencies() { return getCachedValue(frequencies); }, + url, + get cloze() { return getCachedValue(cloze); } + }; +} - let {url} = context; - if (typeof url !== 'string') { url = ''; } +/** + * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry + * @returns {import('anki-templates').KanjiStatGroups} + */ +function getKanjiStats(dictionaryEntry) { + /** @type {import('anki-templates').KanjiStatGroups} */ + const results = {}; + for (const [key, value] of Object.entries(dictionaryEntry.stats)) { + results[key] = value.map(convertKanjiStat); + } + return results; +} - const stats = this.createCachedValue(this._getKanjiStats.bind(this, dictionaryEntry)); - const tags = this.createCachedValue(this._convertTags.bind(this, dictionaryEntry.tags)); - const frequencies = this.createCachedValue(this._getKanjiFrequencies.bind(this, dictionaryEntry)); - const cloze = this.createCachedValue(this._getCloze.bind(this, dictionaryEntry, context)); +/** + * @param {import('dictionary').KanjiStat} kanjiStat + * @returns {import('anki-templates').KanjiStat} + */ +function convertKanjiStat({name, category, content, order, score, dictionary, value}) { + return { + name, + category, + notes: content, + order, + score, + dictionary, + value + }; +} - return { - type: 'kanji', - character, +/** + * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry + * @returns {import('anki-templates').KanjiFrequency[]} + */ +function getKanjiFrequencies(dictionaryEntry) { + /** @type {import('anki-templates').KanjiFrequency[]} */ + const results = []; + for (const {index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue} of dictionaryEntry.frequencies) { + results.push({ + index, dictionary, - onyomi, - kunyomi, - glossary: definitions, - get tags() { return self.getCachedValue(tags); }, - get stats() { return self.getCachedValue(stats); }, - get frequencies() { return self.getCachedValue(frequencies); }, - url, - get cloze() { return self.getCachedValue(cloze); } - }; + dictionaryOrder: { + index: dictionaryIndex, + priority: dictionaryPriority + }, + character, + frequency: displayValue !== null ? displayValue : frequency + }); } + return results; +} - /** - * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry - * @returns {import('anki-templates').KanjiStatGroups} - */ - _getKanjiStats(dictionaryEntry) { - /** @type {import('anki-templates').KanjiStatGroups} */ - const results = {}; - const convertKanjiStatBind = this._convertKanjiStat.bind(this); - for (const [key, value] of Object.entries(dictionaryEntry.stats)) { - results[key] = value.map(convertKanjiStatBind); - } - return results; - } +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @param {import('anki-templates-internal').Context} context + * @param {import('settings').ResultOutputMode} resultOutputMode + * @returns {import('anki-templates').TermDictionaryEntry} + */ +function getTermDefinition(dictionaryEntry, context, resultOutputMode) { + /** @type {import('anki-templates').TermDictionaryEntryType} */ + let type = 'term'; + switch (resultOutputMode) { + case 'group': type = 'termGrouped'; break; + case 'merge': type = 'termMerged'; break; + } + + const {inflectionRuleChainCandidates, score, dictionaryIndex, dictionaryPriority, sourceTermExactMatchCount, definitions} = dictionaryEntry; + + let {url} = context; + if (typeof url !== 'string') { url = ''; } + + const primarySource = getPrimarySource(dictionaryEntry); + + const dictionaryNames = createCachedValue(getTermDictionaryNames.bind(null, dictionaryEntry)); + const commonInfo = createCachedValue(getTermDictionaryEntryCommonInfo.bind(null, dictionaryEntry, type)); + const termTags = createCachedValue(getTermTags.bind(null, dictionaryEntry, type)); + const expressions = createCachedValue(getTermExpressions.bind(null, dictionaryEntry)); + const frequencies = createCachedValue(getTermFrequencies.bind(null, dictionaryEntry)); + const pitches = createCachedValue(getTermPitches.bind(null, dictionaryEntry)); + const phoneticTranscriptions = createCachedValue(getTermPhoneticTranscriptions.bind(null, dictionaryEntry)); + const glossary = createCachedValue(getTermGlossaryArray.bind(null, dictionaryEntry, type)); + const cloze = createCachedValue(getCloze.bind(null, dictionaryEntry, context)); + const furiganaSegments = createCachedValue(getTermFuriganaSegments.bind(null, dictionaryEntry, type)); + const sequence = createCachedValue(getTermDictionaryEntrySequence.bind(null, dictionaryEntry)); + + return { + type, + id: (type === 'term' && definitions.length > 0 ? definitions[0].id : void 0), + source: (primarySource !== null ? primarySource.transformedText : null), + rawSource: (primarySource !== null ? primarySource.originalText : null), + sourceTerm: (type !== 'termMerged' ? (primarySource !== null ? primarySource.deinflectedText : null) : void 0), + inflectionRuleChainCandidates, + score, + isPrimary: (type === 'term' ? dictionaryEntry.isPrimary : void 0), + get sequence() { return getCachedValue(sequence); }, + get dictionary() { return getCachedValue(dictionaryNames)[0]; }, + dictionaryOrder: { + index: dictionaryIndex, + priority: dictionaryPriority + }, + get dictionaryNames() { return getCachedValue(dictionaryNames); }, + get expression() { + const {uniqueTerms} = getCachedValue(commonInfo); + return (type === 'term' || type === 'termGrouped' ? uniqueTerms[0] : uniqueTerms); + }, + get reading() { + const {uniqueReadings} = getCachedValue(commonInfo); + return (type === 'term' || type === 'termGrouped' ? uniqueReadings[0] : uniqueReadings); + }, + get expressions() { return getCachedValue(expressions); }, + get glossary() { return getCachedValue(glossary); }, + get definitionTags() { return type === 'term' ? getCachedValue(commonInfo).definitionTags : void 0; }, + get termTags() { return getCachedValue(termTags); }, + get definitions() { return getCachedValue(commonInfo).definitions; }, + get frequencies() { return getCachedValue(frequencies); }, + get pitches() { return getCachedValue(pitches); }, + get phoneticTranscriptions() { return getCachedValue(phoneticTranscriptions); }, + sourceTermExactMatchCount, + url, + get cloze() { return getCachedValue(cloze); }, + get furiganaSegments() { return getCachedValue(furiganaSegments); } + }; +} - /** - * @param {import('dictionary').KanjiStat} kanjiStat - * @returns {import('anki-templates').KanjiStat} - */ - _convertKanjiStat({name, category, content, order, score, dictionary, value}) { - return { - name, - category, - notes: content, - order, - score, - dictionary, - value - }; +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {string[]} + */ +function getTermDictionaryNames(dictionaryEntry) { + const dictionaryNames = new Set(); + for (const {dictionary} of dictionaryEntry.definitions) { + dictionaryNames.add(dictionary); } + return [...dictionaryNames]; +} - /** - * @param {import('dictionary').KanjiDictionaryEntry} dictionaryEntry - * @returns {import('anki-templates').KanjiFrequency[]} - */ - _getKanjiFrequencies(dictionaryEntry) { - /** @type {import('anki-templates').KanjiFrequency[]} */ - const results = []; - for (const {index, dictionary, dictionaryIndex, dictionaryPriority, character, frequency, displayValue} of dictionaryEntry.frequencies) { - results.push({ - index, - dictionary, - dictionaryOrder: { - index: dictionaryIndex, - priority: dictionaryPriority - }, - character, - frequency: displayValue !== null ? displayValue : frequency - }); +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @param {import('anki-templates').TermDictionaryEntryType} type + * @returns {import('anki-templates').TermDictionaryEntryCommonInfo} + */ +function getTermDictionaryEntryCommonInfo(dictionaryEntry, type) { + const merged = (type === 'termMerged'); + const hasDefinitions = (type !== 'term'); + + /** @type {Set<string>} */ + const allTermsSet = new Set(); + /** @type {Set<string>} */ + const allReadingsSet = new Set(); + for (const {term, reading} of dictionaryEntry.headwords) { + allTermsSet.add(term); + allReadingsSet.add(reading); + } + const uniqueTerms = [...allTermsSet]; + const uniqueReadings = [...allReadingsSet]; + + /** @type {import('anki-templates').TermDefinition[]} */ + const definitions = []; + /** @type {import('anki-templates').Tag[]} */ + const definitionTags = []; + for (const {tags, headwordIndices, entries, dictionary, sequences} of dictionaryEntry.definitions) { + const definitionTags2 = []; + for (const tag of tags) { + definitionTags.push(convertTag(tag)); + definitionTags2.push(convertTag(tag)); } - return results; + if (!hasDefinitions) { continue; } + const only = merged ? getDisambiguations(dictionaryEntry.headwords, headwordIndices, allTermsSet, allReadingsSet) : void 0; + definitions.push({ + sequence: sequences[0], + dictionary, + glossary: entries, + definitionTags: definitionTags2, + only + }); } - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @param {import('anki-templates-internal').Context} context - * @param {import('settings').ResultOutputMode} resultOutputMode - * @returns {import('anki-templates').TermDictionaryEntry} - */ - _getTermDefinition(dictionaryEntry, context, resultOutputMode) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - - /** @type {import('anki-templates').TermDictionaryEntryType} */ - let type = 'term'; - switch (resultOutputMode) { - case 'group': type = 'termGrouped'; break; - case 'merge': type = 'termMerged'; break; - } + return { + uniqueTerms, + uniqueReadings, + definitionTags, + definitions: hasDefinitions ? definitions : void 0 + }; +} - const {inflectionRuleChainCandidates, score, dictionaryIndex, dictionaryPriority, sourceTermExactMatchCount, definitions} = dictionaryEntry; - - let {url} = context; - if (typeof url !== 'string') { url = ''; } - - const primarySource = this._getPrimarySource(dictionaryEntry); - - const dictionaryNames = this.createCachedValue(this._getTermDictionaryNames.bind(this, dictionaryEntry)); - const commonInfo = this.createCachedValue(this._getTermDictionaryEntryCommonInfo.bind(this, dictionaryEntry, type)); - const termTags = this.createCachedValue(this._getTermTags.bind(this, dictionaryEntry, type)); - const expressions = this.createCachedValue(this._getTermExpressions.bind(this, dictionaryEntry)); - const frequencies = this.createCachedValue(this._getTermFrequencies.bind(this, dictionaryEntry)); - const pitches = this.createCachedValue(this._getTermPitches.bind(this, dictionaryEntry)); - const phoneticTranscriptions = this.createCachedValue(this._getTermPhoneticTranscriptions.bind(this, dictionaryEntry)); - const glossary = this.createCachedValue(this._getTermGlossaryArray.bind(this, dictionaryEntry, type)); - const cloze = this.createCachedValue(this._getCloze.bind(this, dictionaryEntry, context)); - const furiganaSegments = this.createCachedValue(this._getTermFuriganaSegments.bind(this, dictionaryEntry, type)); - const sequence = this.createCachedValue(this._getTermDictionaryEntrySequence.bind(this, dictionaryEntry)); - - return { - type, - id: (type === 'term' && definitions.length > 0 ? definitions[0].id : void 0), - source: (primarySource !== null ? primarySource.transformedText : null), - rawSource: (primarySource !== null ? primarySource.originalText : null), - sourceTerm: (type !== 'termMerged' ? (primarySource !== null ? primarySource.deinflectedText : null) : void 0), - inflectionRuleChainCandidates, - score, - isPrimary: (type === 'term' ? dictionaryEntry.isPrimary : void 0), - get sequence() { return self.getCachedValue(sequence); }, - get dictionary() { return self.getCachedValue(dictionaryNames)[0]; }, +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {import('anki-templates').TermFrequency[]} + */ +function getTermFrequencies(dictionaryEntry) { + const results = []; + const {headwords} = dictionaryEntry; + for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue} of dictionaryEntry.frequencies) { + const {term, reading} = headwords[headwordIndex]; + results.push({ + index: results.length, + expressionIndex: headwordIndex, + dictionary, dictionaryOrder: { index: dictionaryIndex, priority: dictionaryPriority }, - get dictionaryNames() { return self.getCachedValue(dictionaryNames); }, - get expression() { - const {uniqueTerms} = self.getCachedValue(commonInfo); - return (type === 'term' || type === 'termGrouped' ? uniqueTerms[0] : uniqueTerms); - }, - get reading() { - const {uniqueReadings} = self.getCachedValue(commonInfo); - return (type === 'term' || type === 'termGrouped' ? uniqueReadings[0] : uniqueReadings); - }, - get expressions() { return self.getCachedValue(expressions); }, - get glossary() { return self.getCachedValue(glossary); }, - get definitionTags() { return type === 'term' ? self.getCachedValue(commonInfo).definitionTags : void 0; }, - get termTags() { return self.getCachedValue(termTags); }, - get definitions() { return self.getCachedValue(commonInfo).definitions; }, - get frequencies() { return self.getCachedValue(frequencies); }, - get pitches() { return self.getCachedValue(pitches); }, - get phoneticTranscriptions() { return self.getCachedValue(phoneticTranscriptions); }, - sourceTermExactMatchCount, - url, - get cloze() { return self.getCachedValue(cloze); }, - get furiganaSegments() { return self.getCachedValue(furiganaSegments); } - }; + expression: term, + reading, + hasReading, + frequency: displayValue !== null ? displayValue : frequency + }); } + return results; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {string[]} - */ - _getTermDictionaryNames(dictionaryEntry) { - const dictionaryNames = new Set(); - for (const {dictionary} of dictionaryEntry.definitions) { - dictionaryNames.add(dictionary); - } - return [...dictionaryNames]; +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {import('anki-templates').TermPitchAccent[]} + */ +function getTermPitches(dictionaryEntry) { + const results = []; + const {headwords} = dictionaryEntry; + for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of dictionaryEntry.pronunciations) { + const {term, reading} = headwords[headwordIndex]; + const pitches = getPronunciationsOfType(pronunciations, 'pitch-accent'); + const cachedPitches = createCachedValue(getTermPitchesInner.bind(null, pitches)); + results.push({ + index: results.length, + expressionIndex: headwordIndex, + dictionary, + dictionaryOrder: { + index: dictionaryIndex, + priority: dictionaryPriority + }, + expression: term, + reading, + get pitches() { return getCachedValue(cachedPitches); } + }); } + return results; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @param {import('anki-templates').TermDictionaryEntryType} type - * @returns {import('anki-templates').TermDictionaryEntryCommonInfo} - */ - _getTermDictionaryEntryCommonInfo(dictionaryEntry, type) { - const merged = (type === 'termMerged'); - const hasDefinitions = (type !== 'term'); - - /** @type {Set<string>} */ - const allTermsSet = new Set(); - /** @type {Set<string>} */ - const allReadingsSet = new Set(); - for (const {term, reading} of dictionaryEntry.headwords) { - allTermsSet.add(term); - allReadingsSet.add(reading); - } - const uniqueTerms = [...allTermsSet]; - const uniqueReadings = [...allReadingsSet]; - - /** @type {import('anki-templates').TermDefinition[]} */ - const definitions = []; - /** @type {import('anki-templates').Tag[]} */ - const definitionTags = []; - for (const {tags, headwordIndices, entries, dictionary, sequences} of dictionaryEntry.definitions) { - const definitionTags2 = []; - for (const tag of tags) { - definitionTags.push(this._convertTag(tag)); - definitionTags2.push(this._convertTag(tag)); - } - if (!hasDefinitions) { continue; } - const only = merged ? DictionaryDataUtil.getDisambiguations(dictionaryEntry.headwords, headwordIndices, allTermsSet, allReadingsSet) : void 0; - definitions.push({ - sequence: sequences[0], - dictionary, - glossary: entries, - definitionTags: definitionTags2, - only - }); - } - - return { - uniqueTerms, - uniqueReadings, - definitionTags, - definitions: hasDefinitions ? definitions : void 0 - }; +/** + * @param {import('dictionary').PitchAccent[]} pitches + * @returns {import('anki-templates').PitchAccent[]} + */ +function getTermPitchesInner(pitches) { + const results = []; + for (const {position, tags} of pitches) { + const cachedTags = createCachedValue(convertTags.bind(null, tags)); + results.push({ + position, + get tags() { return getCachedValue(cachedTags); } + }); } + return results; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {import('anki-templates').TermFrequency[]} - */ - _getTermFrequencies(dictionaryEntry) { - const results = []; - const {headwords} = dictionaryEntry; - for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue} of dictionaryEntry.frequencies) { - const {term, reading} = headwords[headwordIndex]; - results.push({ - index: results.length, - expressionIndex: headwordIndex, - dictionary, - dictionaryOrder: { - index: dictionaryIndex, - priority: dictionaryPriority - }, - expression: term, - reading, - hasReading, - frequency: displayValue !== null ? displayValue : frequency - }); - } - return results; +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {import('anki-templates').TermPhoneticTranscription[]} + */ +function getTermPhoneticTranscriptions(dictionaryEntry) { + const results = []; + const {headwords} = dictionaryEntry; + for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of dictionaryEntry.pronunciations) { + const {term, reading} = headwords[headwordIndex]; + const phoneticTranscriptions = getPronunciationsOfType(pronunciations, 'phonetic-transcription'); + const termPhoneticTranscriptions = getTermPhoneticTranscriptionsInner(phoneticTranscriptions); + results.push({ + index: results.length, + expressionIndex: headwordIndex, + dictionary, + dictionaryOrder: { + index: dictionaryIndex, + priority: dictionaryPriority + }, + expression: term, + reading, + get phoneticTranscriptions() { return termPhoneticTranscriptions; } + }); } - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {import('anki-templates').TermPitchAccent[]} - */ - _getTermPitches(dictionaryEntry) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - const results = []; - const {headwords} = dictionaryEntry; - for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of dictionaryEntry.pronunciations) { - const {term, reading} = headwords[headwordIndex]; - const pitches = DictionaryDataUtil.getPronunciationsOfType(pronunciations, 'pitch-accent'); - const cachedPitches = this.createCachedValue(this._getTermPitchesInner.bind(this, pitches)); - results.push({ - index: results.length, - expressionIndex: headwordIndex, - dictionary, - dictionaryOrder: { - index: dictionaryIndex, - priority: dictionaryPriority - }, - expression: term, - reading, - get pitches() { return self.getCachedValue(cachedPitches); } - }); - } - return results; - } + return results; +} - /** - * @param {import('dictionary').PitchAccent[]} pitches - * @returns {import('anki-templates').PitchAccent[]} - */ - _getTermPitchesInner(pitches) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - const results = []; - for (const {position, tags} of pitches) { - const cachedTags = this.createCachedValue(this._convertTags.bind(this, tags)); - results.push({ - position, - get tags() { return self.getCachedValue(cachedTags); } - }); - } - return results; +/** + * @param {import('dictionary').PhoneticTranscription[]} phoneticTranscriptions + * @returns {import('anki-templates').PhoneticTranscription[]} + */ +function getTermPhoneticTranscriptionsInner(phoneticTranscriptions) { + const results = []; + for (const {ipa, tags} of phoneticTranscriptions) { + const cachedTags = createCachedValue(convertTags.bind(null, tags)); + results.push({ + ipa, + get tags() { return getCachedValue(cachedTags); } + }); } + return results; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {import('anki-templates').TermPhoneticTranscription[]} - */ - _getTermPhoneticTranscriptions(dictionaryEntry) { - const results = []; - const {headwords} = dictionaryEntry; - for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of dictionaryEntry.pronunciations) { - const {term, reading} = headwords[headwordIndex]; - const phoneticTranscriptions = DictionaryDataUtil.getPronunciationsOfType(pronunciations, 'phonetic-transcription'); - const termPhoneticTranscriptions = this._getTermPhoneticTranscriptionsInner(phoneticTranscriptions); - results.push({ - index: results.length, - expressionIndex: headwordIndex, - dictionary, - dictionaryOrder: { - index: dictionaryIndex, - priority: dictionaryPriority - }, - expression: term, - reading, - get phoneticTranscriptions() { return termPhoneticTranscriptions; } - }); - } - - return results; +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {import('anki-templates').TermHeadword[]} + */ +function getTermExpressions(dictionaryEntry) { + const results = []; + const {headwords} = dictionaryEntry; + for (let i = 0, ii = headwords.length; i < ii; ++i) { + const {term, reading, tags, sources: [{deinflectedText}], wordClasses} = headwords[i]; + const termTags = createCachedValue(convertTags.bind(null, tags)); + const frequencies = createCachedValue(getTermExpressionFrequencies.bind(null, dictionaryEntry, i)); + const pitches = createCachedValue(getTermExpressionPitches.bind(null, dictionaryEntry, i)); + const termFrequency = createCachedValue(getTermExpressionTermFrequency.bind(null, termTags)); + const furiganaSegments = createCachedValue(getTermHeadwordFuriganaSegments.bind(null, term, reading)); + const item = { + sourceTerm: deinflectedText, + expression: term, + reading, + get termTags() { return getCachedValue(termTags); }, + get frequencies() { return getCachedValue(frequencies); }, + get pitches() { return getCachedValue(pitches); }, + get furiganaSegments() { return getCachedValue(furiganaSegments); }, + get termFrequency() { return getCachedValue(termFrequency); }, + wordClasses + }; + results.push(item); } + return results; +} - /** - * @param {import('dictionary').PhoneticTranscription[]} phoneticTranscriptions - * @returns {import('anki-templates').PhoneticTranscription[]} - */ - _getTermPhoneticTranscriptionsInner(phoneticTranscriptions) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - const results = []; - for (const {ipa, tags} of phoneticTranscriptions) { - const cachedTags = this.createCachedValue(this._convertTags.bind(this, tags)); - results.push({ - ipa, - get tags() { return self.getCachedValue(cachedTags); } - }); - } - return results; +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @param {number} i + * @returns {import('anki-templates').TermFrequency[]} + */ +function getTermExpressionFrequencies(dictionaryEntry, i) { + const results = []; + const {headwords, frequencies} = dictionaryEntry; + for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue} of frequencies) { + if (headwordIndex !== i) { continue; } + const {term, reading} = headwords[headwordIndex]; + results.push({ + index: results.length, + expressionIndex: headwordIndex, + dictionary, + dictionaryOrder: { + index: dictionaryIndex, + priority: dictionaryPriority + }, + expression: term, + reading, + hasReading, + frequency: displayValue !== null ? displayValue : frequency + }); } + return results; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {import('anki-templates').TermHeadword[]} - */ - _getTermExpressions(dictionaryEntry) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; - const results = []; - const {headwords} = dictionaryEntry; - for (let i = 0, ii = headwords.length; i < ii; ++i) { - const {term, reading, tags, sources: [{deinflectedText}], wordClasses} = headwords[i]; - const termTags = this.createCachedValue(this._convertTags.bind(this, tags)); - const frequencies = this.createCachedValue(this._getTermExpressionFrequencies.bind(this, dictionaryEntry, i)); - const pitches = this.createCachedValue(this._getTermExpressionPitches.bind(this, dictionaryEntry, i)); - const termFrequency = this.createCachedValue(this._getTermExpressionTermFrequency.bind(this, termTags)); - const furiganaSegments = this.createCachedValue(this._getTermHeadwordFuriganaSegments.bind(this, term, reading)); - const item = { - sourceTerm: deinflectedText, - expression: term, - reading, - get termTags() { return self.getCachedValue(termTags); }, - get frequencies() { return self.getCachedValue(frequencies); }, - get pitches() { return self.getCachedValue(pitches); }, - get furiganaSegments() { return self.getCachedValue(furiganaSegments); }, - get termFrequency() { return self.getCachedValue(termFrequency); }, - wordClasses - }; - results.push(item); - } - return results; +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @param {number} i + * @returns {import('anki-templates').TermPitchAccent[]} + */ +function getTermExpressionPitches(dictionaryEntry, i) { + const results = []; + const {headwords, pronunciations: termPronunciations} = dictionaryEntry; + for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of termPronunciations) { + if (headwordIndex !== i) { continue; } + const {term, reading} = headwords[headwordIndex]; + const pitches = getPronunciationsOfType(pronunciations, 'pitch-accent'); + const cachedPitches = createCachedValue(getTermPitchesInner.bind(null, pitches)); + results.push({ + index: results.length, + expressionIndex: headwordIndex, + dictionary, + dictionaryOrder: { + index: dictionaryIndex, + priority: dictionaryPriority + }, + expression: term, + reading, + get pitches() { return getCachedValue(cachedPitches); } + }); } + return results; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @param {number} i - * @returns {import('anki-templates').TermFrequency[]} - */ - _getTermExpressionFrequencies(dictionaryEntry, i) { - const results = []; - const {headwords, frequencies} = dictionaryEntry; - for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, hasReading, frequency, displayValue} of frequencies) { - if (headwordIndex !== i) { continue; } - const {term, reading} = headwords[headwordIndex]; - results.push({ - index: results.length, - expressionIndex: headwordIndex, - dictionary, - dictionaryOrder: { - index: dictionaryIndex, - priority: dictionaryPriority - }, - expression: term, - reading, - hasReading, - frequency: displayValue !== null ? displayValue : frequency - }); - } - return results; - } +/** + * @param {import('anki-templates-internal').CachedValue<import('anki-templates').Tag[]>} cachedTermTags + * @returns {import('anki-templates').TermFrequencyType} + */ +function getTermExpressionTermFrequency(cachedTermTags) { + const termTags = getCachedValue(cachedTermTags); + return getTermFrequency(termTags); +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @param {number} i - * @returns {import('anki-templates').TermPitchAccent[]} - */ - _getTermExpressionPitches(dictionaryEntry, i) { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const self = this; +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @param {import('anki-templates').TermDictionaryEntryType} type + * @returns {import('dictionary-data').TermGlossary[]|undefined} + */ +function getTermGlossaryArray(dictionaryEntry, type) { + if (type === 'term') { const results = []; - const {headwords, pronunciations: termPronunciations} = dictionaryEntry; - for (const {headwordIndex, dictionary, dictionaryIndex, dictionaryPriority, pronunciations} of termPronunciations) { - if (headwordIndex !== i) { continue; } - const {term, reading} = headwords[headwordIndex]; - const pitches = DictionaryDataUtil.getPronunciationsOfType(pronunciations, 'pitch-accent'); - const cachedPitches = this.createCachedValue(this._getTermPitchesInner.bind(this, pitches)); - results.push({ - index: results.length, - expressionIndex: headwordIndex, - dictionary, - dictionaryOrder: { - index: dictionaryIndex, - priority: dictionaryPriority - }, - expression: term, - reading, - get pitches() { return self.getCachedValue(cachedPitches); } - }); + for (const {entries} of dictionaryEntry.definitions) { + results.push(...entries); } return results; } + return void 0; +} - /** - * @param {import('anki-templates-internal').CachedValue<import('anki-templates').Tag[]>} cachedTermTags - * @returns {import('anki-templates').TermFrequencyType} - */ - _getTermExpressionTermFrequency(cachedTermTags) { - const termTags = this.getCachedValue(cachedTermTags); - return DictionaryDataUtil.getTermFrequency(termTags); - } - - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @param {import('anki-templates').TermDictionaryEntryType} type - * @returns {import('dictionary-data').TermGlossary[]|undefined} - */ - _getTermGlossaryArray(dictionaryEntry, type) { - if (type === 'term') { - const results = []; - for (const {entries} of dictionaryEntry.definitions) { - results.push(...entries); - } - return results; - } - return void 0; - } - - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @param {import('anki-templates').TermDictionaryEntryType} type - * @returns {import('anki-templates').Tag[]|undefined} - */ - _getTermTags(dictionaryEntry, type) { - if (type !== 'termMerged') { - const results = []; - for (const {tag} of DictionaryDataUtil.groupTermTags(dictionaryEntry)) { - results.push(this._convertTag(tag)); - } - return results; - } - return void 0; - } - - /** - * @param {import('dictionary').Tag[]} tags - * @returns {import('anki-templates').Tag[]} - */ - _convertTags(tags) { +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @param {import('anki-templates').TermDictionaryEntryType} type + * @returns {import('anki-templates').Tag[]|undefined} + */ +function getTermTags(dictionaryEntry, type) { + if (type !== 'termMerged') { const results = []; - for (const tag of tags) { - results.push(this._convertTag(tag)); + for (const {tag} of groupTermTags(dictionaryEntry)) { + results.push(convertTag(tag)); } return results; } + return void 0; +} - /** - * @param {import('dictionary').Tag} tag - * @returns {import('anki-templates').Tag} - */ - _convertTag({name, category, content, order, score, dictionaries, redundant}) { - return { - name, - category, - notes: (content.length > 0 ? content[0] : ''), - order, - score, - dictionary: (dictionaries.length > 0 ? dictionaries[0] : ''), - redundant - }; +/** + * @param {import('dictionary').Tag[]} tags + * @returns {import('anki-templates').Tag[]} + */ +function convertTags(tags) { + const results = []; + for (const tag of tags) { + results.push(convertTag(tag)); } + return results; +} - /** - * @param {import('dictionary').Tag[]} tags - * @returns {import('anki-templates').PitchTag[]} - */ - _convertPitchTags(tags) { - const results = []; - for (const tag of tags) { - results.push(this._convertPitchTag(tag)); - } - return results; - } +/** + * @param {import('dictionary').Tag} tag + * @returns {import('anki-templates').Tag} + */ +function convertTag({name, category, content, order, score, dictionaries, redundant}) { + return { + name, + category, + notes: (content.length > 0 ? content[0] : ''), + order, + score, + dictionary: (dictionaries.length > 0 ? dictionaries[0] : ''), + redundant + }; +} - /** - * @param {import('dictionary').Tag} tag - * @returns {import('anki-templates').PitchTag} - */ - _convertPitchTag({name, category, content, order, score, dictionaries, redundant}) { - return { - name, - category, - order, - score, - content: [...content], - dictionaries: [...dictionaries], - redundant - }; +/** + * @param {import('dictionary').Tag[]} tags + * @returns {import('anki-templates').PitchTag[]} + */ +function convertPitchTags(tags) { + const results = []; + for (const tag of tags) { + results.push(convertPitchTag(tag)); } + return results; +} - /** - * @param {import('dictionary').DictionaryEntry} dictionaryEntry - * @param {import('anki-templates-internal').Context} context - * @returns {import('anki-templates').Cloze} - */ - _getCloze(dictionaryEntry, context) { - let originalText = ''; - switch (dictionaryEntry.type) { - case 'term': - { - const primarySource = this._getPrimarySource(dictionaryEntry); - if (primarySource !== null) { originalText = primarySource.originalText; } - } - break; - case 'kanji': - originalText = dictionaryEntry.character; - break; - } - - const {sentence} = context; - let text; - let offset; - if (typeof sentence === 'object' && sentence !== null) { - ({text, offset} = sentence); - } - if (typeof text !== 'string') { text = ''; } - if (typeof offset !== 'number') { offset = 0; } - - return { - sentence: text, - prefix: text.substring(0, offset), - body: text.substring(offset, offset + originalText.length), - suffix: text.substring(offset + originalText.length) - }; - } +/** + * @param {import('dictionary').Tag} tag + * @returns {import('anki-templates').PitchTag} + */ +function convertPitchTag({name, category, content, order, score, dictionaries, redundant}) { + return { + name, + category, + order, + score, + content: [...content], + dictionaries: [...dictionaries], + redundant + }; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @param {import('anki-templates').TermDictionaryEntryType} type - * @returns {import('anki-templates').FuriganaSegment[]|undefined} - */ - _getTermFuriganaSegments(dictionaryEntry, type) { - if (type === 'term') { - for (const {term, reading} of dictionaryEntry.headwords) { - return this._getTermHeadwordFuriganaSegments(term, reading); +/** + * @param {import('dictionary').DictionaryEntry} dictionaryEntry + * @param {import('anki-templates-internal').Context} context + * @returns {import('anki-templates').Cloze} + */ +function getCloze(dictionaryEntry, context) { + let originalText = ''; + switch (dictionaryEntry.type) { + case 'term': + { + const primarySource = getPrimarySource(dictionaryEntry); + if (primarySource !== null) { originalText = primarySource.originalText; } } + break; + case 'kanji': + originalText = dictionaryEntry.character; + break; + } + + const {sentence} = context; + let text; + let offset; + if (typeof sentence === 'object' && sentence !== null) { + ({text, offset} = sentence); + } + if (typeof text !== 'string') { text = ''; } + if (typeof offset !== 'number') { offset = 0; } + + return { + sentence: text, + prefix: text.substring(0, offset), + body: text.substring(offset, offset + originalText.length), + suffix: text.substring(offset + originalText.length) + }; +} + +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @param {import('anki-templates').TermDictionaryEntryType} type + * @returns {import('anki-templates').FuriganaSegment[]|undefined} + */ +function getTermFuriganaSegments(dictionaryEntry, type) { + if (type === 'term') { + for (const {term, reading} of dictionaryEntry.headwords) { + return getTermHeadwordFuriganaSegments(term, reading); } - return void 0; } + return void 0; +} - /** - * @param {string} term - * @param {string} reading - * @returns {import('anki-templates').FuriganaSegment[]} - */ - _getTermHeadwordFuriganaSegments(term, reading) { - /** @type {import('anki-templates').FuriganaSegment[]} */ - const result = []; - for (const {text, reading: reading2} of distributeFurigana(term, reading)) { - result.push({text, furigana: reading2}); - } - return result; +/** + * @param {string} term + * @param {string} reading + * @returns {import('anki-templates').FuriganaSegment[]} + */ +function getTermHeadwordFuriganaSegments(term, reading) { + /** @type {import('anki-templates').FuriganaSegment[]} */ + const result = []; + for (const {text, reading: reading2} of distributeFurigana(term, reading)) { + result.push({text, furigana: reading2}); } + return result; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {number} - */ - _getTermDictionaryEntrySequence(dictionaryEntry) { - let hasSequence = false; - let mainSequence = -1; - if (!dictionaryEntry.isPrimary) { return mainSequence; } - for (const {sequences} of dictionaryEntry.definitions) { - const sequence = sequences[0]; - if (!hasSequence) { - mainSequence = sequence; - hasSequence = true; - if (mainSequence === -1) { break; } - } else if (mainSequence !== sequence) { - mainSequence = -1; - break; - } +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {number} + */ +function getTermDictionaryEntrySequence(dictionaryEntry) { + let hasSequence = false; + let mainSequence = -1; + if (!dictionaryEntry.isPrimary) { return mainSequence; } + for (const {sequences} of dictionaryEntry.definitions) { + const sequence = sequences[0]; + if (!hasSequence) { + mainSequence = sequence; + hasSequence = true; + if (mainSequence === -1) { break; } + } else if (mainSequence !== sequence) { + mainSequence = -1; + break; } - return mainSequence; } + return mainSequence; } diff --git a/ext/js/data/sandbox/array-buffer-util.js b/ext/js/data/sandbox/array-buffer-util.js index 1857ec74..487fcd24 100644 --- a/ext/js/data/sandbox/array-buffer-util.js +++ b/ext/js/data/sandbox/array-buffer-util.js @@ -17,61 +17,56 @@ */ /** - * Class containing generic ArrayBuffer utility functions. + * Decodes the contents of an ArrayBuffer using UTF8. + * @param {ArrayBuffer} arrayBuffer The input ArrayBuffer. + * @returns {string} A UTF8-decoded string. */ -export class ArrayBufferUtil { - /** - * Decodes the contents of an ArrayBuffer using UTF8. - * @param {ArrayBuffer} arrayBuffer The input ArrayBuffer. - * @returns {string} A UTF8-decoded string. - */ - static arrayBufferUtf8Decode(arrayBuffer) { - try { - return new TextDecoder('utf-8').decode(arrayBuffer); - } catch (e) { - return decodeURIComponent(escape(this.arrayBufferToBinaryString(arrayBuffer))); - } +export function arrayBufferUtf8Decode(arrayBuffer) { + try { + return new TextDecoder('utf-8').decode(arrayBuffer); + } catch (e) { + return decodeURIComponent(escape(arrayBufferToBinaryString(arrayBuffer))); } +} - /** - * Converts the contents of an ArrayBuffer to a base64 string. - * @param {ArrayBuffer} arrayBuffer The input ArrayBuffer. - * @returns {string} A base64 string representing the binary content. - */ - static arrayBufferToBase64(arrayBuffer) { - return btoa(this.arrayBufferToBinaryString(arrayBuffer)); - } +/** + * Converts the contents of an ArrayBuffer to a base64 string. + * @param {ArrayBuffer} arrayBuffer The input ArrayBuffer. + * @returns {string} A base64 string representing the binary content. + */ +export function arrayBufferToBase64(arrayBuffer) { + return btoa(arrayBufferToBinaryString(arrayBuffer)); +} - /** - * Converts the contents of an ArrayBuffer to a binary string. - * @param {ArrayBuffer} arrayBuffer The input ArrayBuffer. - * @returns {string} A string representing the binary content. - */ - static arrayBufferToBinaryString(arrayBuffer) { - const bytes = new Uint8Array(arrayBuffer); - try { - return String.fromCharCode(...bytes); - } catch (e) { - let binary = ''; - for (let i = 0, ii = bytes.byteLength; i < ii; ++i) { - binary += String.fromCharCode(bytes[i]); - } - return binary; +/** + * Converts the contents of an ArrayBuffer to a binary string. + * @param {ArrayBuffer} arrayBuffer The input ArrayBuffer. + * @returns {string} A string representing the binary content. + */ +export function arrayBufferToBinaryString(arrayBuffer) { + const bytes = new Uint8Array(arrayBuffer); + try { + return String.fromCharCode(...bytes); + } catch (e) { + let binary = ''; + for (let i = 0, ii = bytes.byteLength; i < ii; ++i) { + binary += String.fromCharCode(bytes[i]); } + return binary; } +} - /** - * Converts a base64 string to an ArrayBuffer. - * @param {string} content The binary content string encoded in base64. - * @returns {ArrayBuffer} A new `ArrayBuffer` object corresponding to the specified content. - */ - static base64ToArrayBuffer(content) { - const binaryContent = atob(content); - const length = binaryContent.length; - const array = new Uint8Array(length); - for (let i = 0; i < length; ++i) { - array[i] = binaryContent.charCodeAt(i); - } - return array.buffer; +/** + * Converts a base64 string to an ArrayBuffer. + * @param {string} content The binary content string encoded in base64. + * @returns {ArrayBuffer} A new `ArrayBuffer` object corresponding to the specified content. + */ +export function base64ToArrayBuffer(content) { + const binaryContent = atob(content); + const length = binaryContent.length; + const array = new Uint8Array(length); + for (let i = 0; i < length; ++i) { + array[i] = binaryContent.charCodeAt(i); } + return array.buffer; } diff --git a/ext/js/data/sandbox/string-util.js b/ext/js/data/sandbox/string-util.js index 3bc1c549..45e52f08 100644 --- a/ext/js/data/sandbox/string-util.js +++ b/ext/js/data/sandbox/string-util.js @@ -17,59 +17,54 @@ */ /** - * Class containing generic string utility functions. + * Reads code points from a string in the forward direction. + * @param {string} text The text to read the code points from. + * @param {number} position The index of the first character to read. + * @param {number} count The number of code points to read. + * @returns {string} The code points from the string. */ -export class StringUtil { - /** - * Reads code points from a string in the forward direction. - * @param {string} text The text to read the code points from. - * @param {number} position The index of the first character to read. - * @param {number} count The number of code points to read. - * @returns {string} The code points from the string. - */ - static readCodePointsForward(text, position, count) { - const textLength = text.length; - let result = ''; - for (; count > 0; --count) { - const char = text[position]; - result += char; - if (++position >= textLength) { break; } - const charCode = char.charCodeAt(0); - if (charCode >= 0xd800 && charCode < 0xdc00) { // charCode is a high surrogate code - const char2 = text[position]; - const charCode2 = char2.charCodeAt(0); - if (charCode2 >= 0xdc00 && charCode2 < 0xe000) { // charCode2 is a low surrogate code - result += char2; - if (++position >= textLength) { break; } - } +export function readCodePointsForward(text, position, count) { + const textLength = text.length; + let result = ''; + for (; count > 0; --count) { + const char = text[position]; + result += char; + if (++position >= textLength) { break; } + const charCode = char.charCodeAt(0); + if (charCode >= 0xd800 && charCode < 0xdc00) { // charCode is a high surrogate code + const char2 = text[position]; + const charCode2 = char2.charCodeAt(0); + if (charCode2 >= 0xdc00 && charCode2 < 0xe000) { // charCode2 is a low surrogate code + result += char2; + if (++position >= textLength) { break; } } } - return result; } + return result; +} - /** - * Reads code points from a string in the backward direction. - * @param {string} text The text to read the code points from. - * @param {number} position The index of the first character to read. - * @param {number} count The number of code points to read. - * @returns {string} The code points from the string. - */ - static readCodePointsBackward(text, position, count) { - let result = ''; - for (; count > 0; --count) { - const char = text[position]; - result = char + result; - if (--position < 0) { break; } - const charCode = char.charCodeAt(0); - if (charCode >= 0xdc00 && charCode < 0xe000) { // charCode is a low surrogate code - const char2 = text[position]; - const charCode2 = char2.charCodeAt(0); - if (charCode2 >= 0xd800 && charCode2 < 0xdc00) { // charCode2 is a high surrogate code - result = char2 + result; - if (--position < 0) { break; } - } +/** + * Reads code points from a string in the backward direction. + * @param {string} text The text to read the code points from. + * @param {number} position The index of the first character to read. + * @param {number} count The number of code points to read. + * @returns {string} The code points from the string. + */ +export function readCodePointsBackward(text, position, count) { + let result = ''; + for (; count > 0; --count) { + const char = text[position]; + result = char + result; + if (--position < 0) { break; } + const charCode = char.charCodeAt(0); + if (charCode >= 0xdc00 && charCode < 0xe000) { // charCode is a low surrogate code + const char2 = text[position]; + const charCode2 = char2.charCodeAt(0); + if (charCode2 >= 0xd800 && charCode2 < 0xdc00) { // charCode2 is a high surrogate code + result = char2 + result; + if (--position < 0) { break; } } } - return result; } + return result; } diff --git a/ext/js/dictionary/dictionary-data-util.js b/ext/js/dictionary/dictionary-data-util.js index 9b49c7af..a2a106cc 100644 --- a/ext/js/dictionary/dictionary-data-util.js +++ b/ext/js/dictionary/dictionary-data-util.js @@ -16,411 +16,409 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -export class DictionaryDataUtil { - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {import('dictionary-data-util').TagGroup[]} - */ - static groupTermTags(dictionaryEntry) { - const {headwords} = dictionaryEntry; - const headwordCount = headwords.length; - const uniqueCheck = (headwordCount > 1); - const resultsIndexMap = new Map(); - const results = []; - for (let i = 0; i < headwordCount; ++i) { - const {tags} = headwords[i]; - for (const tag of tags) { - if (uniqueCheck) { - const {name, category, content, dictionaries} = tag; - const key = this._createMapKey([name, category, content, dictionaries]); - const index = resultsIndexMap.get(key); - if (typeof index !== 'undefined') { - const existingItem = results[index]; - existingItem.headwordIndices.push(i); - continue; - } - resultsIndexMap.set(key, results.length); +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {import('dictionary-data-util').TagGroup[]} + */ +export function groupTermTags(dictionaryEntry) { + const {headwords} = dictionaryEntry; + const headwordCount = headwords.length; + const uniqueCheck = (headwordCount > 1); + const resultsIndexMap = new Map(); + const results = []; + for (let i = 0; i < headwordCount; ++i) { + const {tags} = headwords[i]; + for (const tag of tags) { + if (uniqueCheck) { + const {name, category, content, dictionaries} = tag; + const key = createMapKey([name, category, content, dictionaries]); + const index = resultsIndexMap.get(key); + if (typeof index !== 'undefined') { + const existingItem = results[index]; + existingItem.headwordIndices.push(i); + continue; } - - const item = {tag, headwordIndices: [i]}; - results.push(item); + resultsIndexMap.set(key, results.length); } + + const item = {tag, headwordIndices: [i]}; + results.push(item); } - return results; } + return results; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {import('dictionary-data-util').DictionaryFrequency<import('dictionary-data-util').TermFrequency>[]} - */ - static groupTermFrequencies(dictionaryEntry) { - const {headwords, frequencies: sourceFrequencies} = dictionaryEntry; - - /** @type {import('dictionary-data-util').TermFrequenciesMap1} */ - const map1 = new Map(); - for (const {headwordIndex, dictionary, hasReading, frequency, displayValue} of sourceFrequencies) { - const {term, reading} = headwords[headwordIndex]; - - let map2 = map1.get(dictionary); - if (typeof map2 === 'undefined') { - map2 = new Map(); - map1.set(dictionary, map2); - } - - const readingKey = hasReading ? reading : null; - const key = this._createMapKey([term, readingKey]); - let frequencyData = map2.get(key); - if (typeof frequencyData === 'undefined') { - frequencyData = {term, reading: readingKey, values: new Map()}; - map2.set(key, frequencyData); - } - - frequencyData.values.set(this._createMapKey([frequency, displayValue]), {frequency, displayValue}); +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {import('dictionary-data-util').DictionaryFrequency<import('dictionary-data-util').TermFrequency>[]} + */ +export function groupTermFrequencies(dictionaryEntry) { + const {headwords, frequencies: sourceFrequencies} = dictionaryEntry; + + /** @type {import('dictionary-data-util').TermFrequenciesMap1} */ + const map1 = new Map(); + for (const {headwordIndex, dictionary, hasReading, frequency, displayValue} of sourceFrequencies) { + const {term, reading} = headwords[headwordIndex]; + + let map2 = map1.get(dictionary); + if (typeof map2 === 'undefined') { + map2 = new Map(); + map1.set(dictionary, map2); } - const results = []; - for (const [dictionary, map2] of map1.entries()) { - const frequencies = []; - for (const {term, reading, values} of map2.values()) { - frequencies.push({ - term, - reading, - values: [...values.values()] - }); - } - results.push({dictionary, frequencies}); + const readingKey = hasReading ? reading : null; + const key = createMapKey([term, readingKey]); + let frequencyData = map2.get(key); + if (typeof frequencyData === 'undefined') { + frequencyData = {term, reading: readingKey, values: new Map()}; + map2.set(key, frequencyData); } - return results; - } - /** - * @param {import('dictionary').KanjiFrequency[]} sourceFrequencies - * @returns {import('dictionary-data-util').DictionaryFrequency<import('dictionary-data-util').KanjiFrequency>[]} - */ - static groupKanjiFrequencies(sourceFrequencies) { - /** @type {import('dictionary-data-util').KanjiFrequenciesMap1} */ - const map1 = new Map(); - for (const {dictionary, character, frequency, displayValue} of sourceFrequencies) { - let map2 = map1.get(dictionary); - if (typeof map2 === 'undefined') { - map2 = new Map(); - map1.set(dictionary, map2); - } - - let frequencyData = map2.get(character); - if (typeof frequencyData === 'undefined') { - frequencyData = {character, values: new Map()}; - map2.set(character, frequencyData); - } + frequencyData.values.set(createMapKey([frequency, displayValue]), {frequency, displayValue}); + } - frequencyData.values.set(this._createMapKey([frequency, displayValue]), {frequency, displayValue}); + const results = []; + for (const [dictionary, map2] of map1.entries()) { + const frequencies = []; + for (const {term, reading, values} of map2.values()) { + frequencies.push({ + term, + reading, + values: [...values.values()] + }); } - - const results = []; - for (const [dictionary, map2] of map1.entries()) { - const frequencies = []; - for (const {character, values} of map2.values()) { - frequencies.push({ - character, - values: [...values.values()] - }); - } - results.push({dictionary, frequencies}); - } - return results; + results.push({dictionary, frequencies}); } + return results; +} - /** - * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry - * @returns {import('dictionary-data-util').DictionaryGroupedPronunciations[]} - */ - static getGroupedPronunciations(dictionaryEntry) { - const {headwords, pronunciations: termPronunciations} = dictionaryEntry; - - const allTerms = new Set(); - const allReadings = new Set(); - for (const {term, reading} of headwords) { - allTerms.add(term); - allReadings.add(reading); +/** + * @param {import('dictionary').KanjiFrequency[]} sourceFrequencies + * @returns {import('dictionary-data-util').DictionaryFrequency<import('dictionary-data-util').KanjiFrequency>[]} + */ +export function groupKanjiFrequencies(sourceFrequencies) { + /** @type {import('dictionary-data-util').KanjiFrequenciesMap1} */ + const map1 = new Map(); + for (const {dictionary, character, frequency, displayValue} of sourceFrequencies) { + let map2 = map1.get(dictionary); + if (typeof map2 === 'undefined') { + map2 = new Map(); + map1.set(dictionary, map2); } - /** @type {Map<string, import('dictionary-data-util').GroupedPronunciationInternal[]>} */ - const groupedPronunciationsMap = new Map(); - for (const {headwordIndex, dictionary, pronunciations} of termPronunciations) { - const {term, reading} = headwords[headwordIndex]; - let dictionaryGroupedPronunciationList = groupedPronunciationsMap.get(dictionary); - if (typeof dictionaryGroupedPronunciationList === 'undefined') { - dictionaryGroupedPronunciationList = []; - groupedPronunciationsMap.set(dictionary, dictionaryGroupedPronunciationList); - } - for (const pronunciation of pronunciations) { - let groupedPronunciation = this._findExistingGroupedPronunciation(reading, pronunciation, dictionaryGroupedPronunciationList); - if (groupedPronunciation === null) { - groupedPronunciation = { - pronunciation, - terms: new Set(), - reading - }; - dictionaryGroupedPronunciationList.push(groupedPronunciation); - } - groupedPronunciation.terms.add(term); - } + let frequencyData = map2.get(character); + if (typeof frequencyData === 'undefined') { + frequencyData = {character, values: new Map()}; + map2.set(character, frequencyData); } - /** @type {import('dictionary-data-util').DictionaryGroupedPronunciations[]} */ - const results2 = []; - const multipleReadings = (allReadings.size > 1); - for (const [dictionary, dictionaryGroupedPronunciationList] of groupedPronunciationsMap.entries()) { - /** @type {import('dictionary-data-util').GroupedPronunciation[]} */ - const pronunciations2 = []; - for (const groupedPronunciation of dictionaryGroupedPronunciationList) { - const {pronunciation, terms, reading} = groupedPronunciation; - const exclusiveTerms = !this._areSetsEqual(terms, allTerms) ? this._getSetIntersection(terms, allTerms) : []; - const exclusiveReadings = []; - if (multipleReadings) { - exclusiveReadings.push(reading); - } - pronunciations2.push({ - pronunciation, - terms: [...terms], - reading, - exclusiveTerms, - exclusiveReadings - }); - } - - results2.push({dictionary, pronunciations: pronunciations2}); - } - return results2; + frequencyData.values.set(createMapKey([frequency, displayValue]), {frequency, displayValue}); } - /** - * @template {import('dictionary').PronunciationType} T - * @param {import('dictionary').Pronunciation[]} pronunciations - * @param {T} type - * @returns {import('dictionary').PronunciationGeneric<T>[]} - */ - static getPronunciationsOfType(pronunciations, type) { - /** @type {import('dictionary').PronunciationGeneric<T>[]} */ - const results = []; - for (const pronunciation of pronunciations) { - if (pronunciation.type !== type) { continue; } - // This is type safe, but for some reason the cast is needed. - results.push(/** @type {import('dictionary').PronunciationGeneric<T>} */ (pronunciation)); + const results = []; + for (const [dictionary, map2] of map1.entries()) { + const frequencies = []; + for (const {character, values} of map2.values()) { + frequencies.push({ + character, + values: [...values.values()] + }); } - return results; + results.push({dictionary, frequencies}); } + return results; +} - /** - * @param {import('dictionary').Tag[]|import('anki-templates').Tag[]} termTags - * @returns {import('dictionary-data-util').TermFrequencyType} - */ - static getTermFrequency(termTags) { - let totalScore = 0; - for (const {score} of termTags) { - totalScore += score; - } - if (totalScore > 0) { - return 'popular'; - } else if (totalScore < 0) { - return 'rare'; - } else { - return 'normal'; - } +/** + * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry + * @returns {import('dictionary-data-util').DictionaryGroupedPronunciations[]} + */ +export function getGroupedPronunciations(dictionaryEntry) { + const {headwords, pronunciations: termPronunciations} = dictionaryEntry; + + const allTerms = new Set(); + const allReadings = new Set(); + for (const {term, reading} of headwords) { + allTerms.add(term); + allReadings.add(reading); } - /** - * @param {import('dictionary').TermHeadword[]} headwords - * @param {number[]} headwordIndices - * @param {Set<string>} allTermsSet - * @param {Set<string>} allReadingsSet - * @returns {string[]} - */ - static getDisambiguations(headwords, headwordIndices, allTermsSet, allReadingsSet) { - if (allTermsSet.size <= 1 && allReadingsSet.size <= 1) { return []; } - - /** @type {Set<string>} */ - const terms = new Set(); - /** @type {Set<string>} */ - const readings = new Set(); - for (const headwordIndex of headwordIndices) { - const {term, reading} = headwords[headwordIndex]; - terms.add(term); - readings.add(reading); + /** @type {Map<string, import('dictionary-data-util').GroupedPronunciationInternal[]>} */ + const groupedPronunciationsMap = new Map(); + for (const {headwordIndex, dictionary, pronunciations} of termPronunciations) { + const {term, reading} = headwords[headwordIndex]; + let dictionaryGroupedPronunciationList = groupedPronunciationsMap.get(dictionary); + if (typeof dictionaryGroupedPronunciationList === 'undefined') { + dictionaryGroupedPronunciationList = []; + groupedPronunciationsMap.set(dictionary, dictionaryGroupedPronunciationList); } - - /** @type {string[]} */ - const disambiguations = []; - const addTerms = !this._areSetsEqual(terms, allTermsSet); - const addReadings = !this._areSetsEqual(readings, allReadingsSet); - if (addTerms) { - disambiguations.push(...this._getSetIntersection(terms, allTermsSet)); - } - if (addReadings) { - if (addTerms) { - for (const term of terms) { - readings.delete(term); - } + for (const pronunciation of pronunciations) { + let groupedPronunciation = findExistingGroupedPronunciation(reading, pronunciation, dictionaryGroupedPronunciationList); + if (groupedPronunciation === null) { + groupedPronunciation = { + pronunciation, + terms: new Set(), + reading + }; + dictionaryGroupedPronunciationList.push(groupedPronunciation); } - disambiguations.push(...this._getSetIntersection(readings, allReadingsSet)); + groupedPronunciation.terms.add(term); } - return disambiguations; } - /** - * @param {string[]} wordClasses - * @returns {boolean} - */ - static isNonNounVerbOrAdjective(wordClasses) { - let isVerbOrAdjective = false; - let isSuruVerb = false; - let isNoun = false; - for (const wordClass of wordClasses) { - switch (wordClass) { - case 'v1': - case 'v5': - case 'vk': - case 'vz': - case 'adj-i': - isVerbOrAdjective = true; - break; - case 'vs': - isVerbOrAdjective = true; - isSuruVerb = true; - // falls through - case 'n': - isNoun = true; - break; + /** @type {import('dictionary-data-util').DictionaryGroupedPronunciations[]} */ + const results2 = []; + const multipleReadings = (allReadings.size > 1); + for (const [dictionary, dictionaryGroupedPronunciationList] of groupedPronunciationsMap.entries()) { + /** @type {import('dictionary-data-util').GroupedPronunciation[]} */ + const pronunciations2 = []; + for (const groupedPronunciation of dictionaryGroupedPronunciationList) { + const {pronunciation, terms, reading} = groupedPronunciation; + const exclusiveTerms = !areSetsEqual(terms, allTerms) ? getSetIntersection(terms, allTerms) : []; + const exclusiveReadings = []; + if (multipleReadings) { + exclusiveReadings.push(reading); } + pronunciations2.push({ + pronunciation, + terms: [...terms], + reading, + exclusiveTerms, + exclusiveReadings + }); } - return isVerbOrAdjective && !(isSuruVerb && isNoun); + + results2.push({dictionary, pronunciations: pronunciations2}); } + return results2; +} - // Private +/** + * @template {import('dictionary').PronunciationType} T + * @param {import('dictionary').Pronunciation[]} pronunciations + * @param {T} type + * @returns {import('dictionary').PronunciationGeneric<T>[]} + */ +export function getPronunciationsOfType(pronunciations, type) { + /** @type {import('dictionary').PronunciationGeneric<T>[]} */ + const results = []; + for (const pronunciation of pronunciations) { + if (pronunciation.type !== type) { continue; } + // This is type safe, but for some reason the cast is needed. + results.push(/** @type {import('dictionary').PronunciationGeneric<T>} */ (pronunciation)); + } + return results; +} - /** - * @param {string} reading - * @param {import('dictionary').Pronunciation} pronunciation - * @param {import('dictionary-data-util').GroupedPronunciationInternal[]} groupedPronunciationList - * @returns {?import('dictionary-data-util').GroupedPronunciationInternal} - */ - static _findExistingGroupedPronunciation(reading, pronunciation, groupedPronunciationList) { - const existingGroupedPronunciation = groupedPronunciationList.find((groupedPronunciation) => { - return groupedPronunciation.reading === reading && this._arePronunciationsEquivalent(groupedPronunciation, pronunciation); - }); +/** + * @param {import('dictionary').Tag[]|import('anki-templates').Tag[]} termTags + * @returns {import('dictionary-data-util').TermFrequencyType} + */ +export function getTermFrequency(termTags) { + let totalScore = 0; + for (const {score} of termTags) { + totalScore += score; + } + if (totalScore > 0) { + return 'popular'; + } else if (totalScore < 0) { + return 'rare'; + } else { + return 'normal'; + } +} - return existingGroupedPronunciation || null; +/** + * @param {import('dictionary').TermHeadword[]} headwords + * @param {number[]} headwordIndices + * @param {Set<string>} allTermsSet + * @param {Set<string>} allReadingsSet + * @returns {string[]} + */ +export function getDisambiguations(headwords, headwordIndices, allTermsSet, allReadingsSet) { + if (allTermsSet.size <= 1 && allReadingsSet.size <= 1) { return []; } + + /** @type {Set<string>} */ + const terms = new Set(); + /** @type {Set<string>} */ + const readings = new Set(); + for (const headwordIndex of headwordIndices) { + const {term, reading} = headwords[headwordIndex]; + terms.add(term); + readings.add(reading); } - /** - * @param {import('dictionary-data-util').GroupedPronunciationInternal} groupedPronunciation - * @param {import('dictionary').Pronunciation} pronunciation2 - * @returns {boolean} - */ - static _arePronunciationsEquivalent({pronunciation: pronunciation1}, pronunciation2) { - if ( - pronunciation1.type !== pronunciation2.type || - !this._areTagListsEqual(pronunciation1.tags, pronunciation2.tags) - ) { - return false; - } - switch (pronunciation1.type) { - case 'pitch-accent': - { - // This cast is valid based on the type check at the start of the function. - const pitchAccent2 = /** @type {import('dictionary').PitchAccent} */ (pronunciation2); - return ( - pronunciation1.position === pitchAccent2.position && - this._areArraysEqual(pronunciation1.nasalPositions, pitchAccent2.nasalPositions) && - this._areArraysEqual(pronunciation1.devoicePositions, pitchAccent2.devoicePositions) - ); - } - case 'phonetic-transcription': - { - // This cast is valid based on the type check at the start of the function. - const phoneticTranscription2 = /** @type {import('dictionary').PhoneticTranscription} */ (pronunciation2); - return pronunciation1.ipa === phoneticTranscription2.ipa; + /** @type {string[]} */ + const disambiguations = []; + const addTerms = !areSetsEqual(terms, allTermsSet); + const addReadings = !areSetsEqual(readings, allReadingsSet); + if (addTerms) { + disambiguations.push(...getSetIntersection(terms, allTermsSet)); + } + if (addReadings) { + if (addTerms) { + for (const term of terms) { + readings.delete(term); } } - return true; + disambiguations.push(...getSetIntersection(readings, allReadingsSet)); } + return disambiguations; +} - /** - * @template [T=unknown] - * @param {T[]} array1 - * @param {T[]} array2 - * @returns {boolean} - */ - static _areArraysEqual(array1, array2) { - const ii = array1.length; - if (ii !== array2.length) { return false; } - for (let i = 0; i < ii; ++i) { - if (array1[i] !== array2[i]) { return false; } +/** + * @param {string[]} wordClasses + * @returns {boolean} + */ +export function isNonNounVerbOrAdjective(wordClasses) { + let isVerbOrAdjective = false; + let isSuruVerb = false; + let isNoun = false; + for (const wordClass of wordClasses) { + switch (wordClass) { + case 'v1': + case 'v5': + case 'vk': + case 'vz': + case 'adj-i': + isVerbOrAdjective = true; + break; + case 'vs': + isVerbOrAdjective = true; + isSuruVerb = true; + // falls through + case 'n': + isNoun = true; + break; } - return true; } + return isVerbOrAdjective && !(isSuruVerb && isNoun); +} - /** - * @param {import('dictionary').Tag[]} tagList1 - * @param {import('dictionary').Tag[]} tagList2 - * @returns {boolean} - */ - static _areTagListsEqual(tagList1, tagList2) { - const ii = tagList1.length; - if (tagList2.length !== ii) { return false; } - - for (let i = 0; i < ii; ++i) { - const tag1 = tagList1[i]; - const tag2 = tagList2[i]; - if (tag1.name !== tag2.name || !this._areArraysEqual(tag1.dictionaries, tag2.dictionaries)) { - return false; - } +// Private + +/** + * @param {string} reading + * @param {import('dictionary').Pronunciation} pronunciation + * @param {import('dictionary-data-util').GroupedPronunciationInternal[]} groupedPronunciationList + * @returns {?import('dictionary-data-util').GroupedPronunciationInternal} + */ +function findExistingGroupedPronunciation(reading, pronunciation, groupedPronunciationList) { + const existingGroupedPronunciation = groupedPronunciationList.find((groupedPronunciation) => { + return groupedPronunciation.reading === reading && arePronunciationsEquivalent(groupedPronunciation, pronunciation); + }); + + return existingGroupedPronunciation || null; +} + +/** + * @param {import('dictionary-data-util').GroupedPronunciationInternal} groupedPronunciation + * @param {import('dictionary').Pronunciation} pronunciation2 + * @returns {boolean} + */ +function arePronunciationsEquivalent({pronunciation: pronunciation1}, pronunciation2) { + if ( + pronunciation1.type !== pronunciation2.type || + !areTagListsEqual(pronunciation1.tags, pronunciation2.tags) + ) { + return false; + } + switch (pronunciation1.type) { + case 'pitch-accent': + { + // This cast is valid based on the type check at the start of the function. + const pitchAccent2 = /** @type {import('dictionary').PitchAccent} */ (pronunciation2); + return ( + pronunciation1.position === pitchAccent2.position && + areArraysEqual(pronunciation1.nasalPositions, pitchAccent2.nasalPositions) && + areArraysEqual(pronunciation1.devoicePositions, pitchAccent2.devoicePositions) + ); + } + case 'phonetic-transcription': + { + // This cast is valid based on the type check at the start of the function. + const phoneticTranscription2 = /** @type {import('dictionary').PhoneticTranscription} */ (pronunciation2); + return pronunciation1.ipa === phoneticTranscription2.ipa; } + } + return true; +} - return true; +/** + * @template [T=unknown] + * @param {T[]} array1 + * @param {T[]} array2 + * @returns {boolean} + */ +function areArraysEqual(array1, array2) { + const ii = array1.length; + if (ii !== array2.length) { return false; } + for (let i = 0; i < ii; ++i) { + if (array1[i] !== array2[i]) { return false; } } + return true; +} - /** - * @template [T=unknown] - * @param {Set<T>} set1 - * @param {Set<T>} set2 - * @returns {boolean} - */ - static _areSetsEqual(set1, set2) { - if (set1.size !== set2.size) { +/** + * @param {import('dictionary').Tag[]} tagList1 + * @param {import('dictionary').Tag[]} tagList2 + * @returns {boolean} + */ +function areTagListsEqual(tagList1, tagList2) { + const ii = tagList1.length; + if (tagList2.length !== ii) { return false; } + + for (let i = 0; i < ii; ++i) { + const tag1 = tagList1[i]; + const tag2 = tagList2[i]; + if (tag1.name !== tag2.name || !areArraysEqual(tag1.dictionaries, tag2.dictionaries)) { return false; } + } - for (const value of set1) { - if (!set2.has(value)) { - return false; - } - } + return true; +} - return true; +/** + * @template [T=unknown] + * @param {Set<T>} set1 + * @param {Set<T>} set2 + * @returns {boolean} + */ +function areSetsEqual(set1, set2) { + if (set1.size !== set2.size) { + return false; } - /** - * @template [T=unknown] - * @param {Set<T>} set1 - * @param {Set<T>} set2 - * @returns {T[]} - */ - static _getSetIntersection(set1, set2) { - const result = []; - for (const value of set1) { - if (set2.has(value)) { - result.push(value); - } + for (const value of set1) { + if (!set2.has(value)) { + return false; } - return result; } - /** - * @param {unknown[]} array - * @returns {string} - */ - static _createMapKey(array) { - return JSON.stringify(array); + return true; +} + +/** + * @template [T=unknown] + * @param {Set<T>} set1 + * @param {Set<T>} set2 + * @returns {T[]} + */ +function getSetIntersection(set1, set2) { + const result = []; + for (const value of set1) { + if (set2.has(value)) { + result.push(value); + } } + return result; +} + +/** + * @param {unknown[]} array + * @returns {string} + */ +function createMapKey(array) { + return JSON.stringify(array); } diff --git a/ext/js/dictionary/dictionary-importer.js b/ext/js/dictionary/dictionary-importer.js index 0feed8b2..eed7d9dd 100644 --- a/ext/js/dictionary/dictionary-importer.js +++ b/ext/js/dictionary/dictionary-importer.js @@ -24,11 +24,11 @@ import { ZipReader as ZipReader0, configure } from '../../lib/zip.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'; -import {MediaUtil} from '../media/media-util.js'; +import {stringReverse} from '../core/utilities.js'; +import {getFileExtensionFromImageMediaType, getImageMediaTypeFromFileName} from '../media/media-util.js'; const ajvSchemas = /** @type {import('dictionary-importer').CompiledSchemaValidators} */ (/** @type {unknown} */ (ajvSchemas0)); const BlobWriter = /** @type {typeof import('@zip.js/zip.js').BlobWriter} */ (/** @type {unknown} */ (BlobWriter0)); @@ -560,7 +560,7 @@ export class DictionaryImporter { // Check if already added let mediaData = media.get(path); if (typeof mediaData !== 'undefined') { - if (MediaUtil.getFileExtensionFromImageMediaType(mediaData.mediaType) === null) { + if (getFileExtensionFromImageMediaType(mediaData.mediaType) === null) { throw createError('Media file is not a valid image'); } return mediaData; @@ -575,7 +575,7 @@ export class DictionaryImporter { // Load file content let content = await (await this._getData(file, new BlobWriter())).arrayBuffer(); - const mediaType = MediaUtil.getImageMediaTypeFromFileName(path); + const mediaType = getImageMediaTypeFromFileName(path); if (mediaType === null) { throw createError('Could not determine media type for image'); } diff --git a/ext/js/display/display-anki.js b/ext/js/display/display-anki.js index 5433142d..68d28d33 100644 --- a/ext/js/display/display-anki.js +++ b/ext/js/display/display-anki.js @@ -20,7 +20,7 @@ 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 {isNoteDataValid} from '../data/anki-util.js'; import {PopupMenu} from '../dom/popup-menu.js'; import {querySelectorNotNull} from '../dom/query-selector.js'; import {TemplateRendererProxy} from '../templates/template-renderer-proxy.js'; @@ -676,7 +676,7 @@ export class DisplayAnki { _getAnkiNoteInfoForceValue(notes, canAdd) { const results = []; for (const note of notes) { - const valid = AnkiUtil.isNoteDataValid(note); + const valid = isNoteDataValid(note); results.push({canAdd, valid, noteIds: null}); } return results; diff --git a/ext/js/display/display-content-manager.js b/ext/js/display/display-content-manager.js index d13dffb3..4465ce3e 100644 --- a/ext/js/display/display-content-manager.js +++ b/ext/js/display/display-content-manager.js @@ -17,7 +17,7 @@ */ import {EventListenerCollection} from '../core/event-listener-collection.js'; -import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; +import {base64ToArrayBuffer} from '../data/sandbox/array-buffer-util.js'; import {yomitan} from '../yomitan.js'; /** @@ -143,7 +143,7 @@ export class DisplayContentManager { const datas = await yomitan.api.getMedia([{path, dictionary}]); if (token === this._token && datas.length > 0) { const data = datas[0]; - const buffer = ArrayBufferUtil.base64ToArrayBuffer(data.content); + const buffer = base64ToArrayBuffer(data.content); const blob = new Blob([buffer], {type: data.mediaType}); const url = URL.createObjectURL(blob); return {data, url}; diff --git a/ext/js/display/display-generator.js b/ext/js/display/display-generator.js index eef58bb0..01f6f38b 100644 --- a/ext/js/display/display-generator.js +++ b/ext/js/display/display-generator.js @@ -18,11 +18,11 @@ import {ExtensionError} from '../core/extension-error.js'; import {isObject} from '../core/utilities.js'; -import {DictionaryDataUtil} from '../dictionary/dictionary-data-util.js'; +import {getDisambiguations, getGroupedPronunciations, getTermFrequency, groupKanjiFrequencies, groupTermFrequencies, groupTermTags, isNonNounVerbOrAdjective} from '../dictionary/dictionary-data-util.js'; import {HtmlTemplateCollection} from '../dom/html-template-collection.js'; import {distributeFurigana, getKanaMorae, getPitchCategory, isCodePointKanji, isStringPartiallyJapanese} from '../language/japanese.js'; import {yomitan} from '../yomitan.js'; -import {PronunciationGenerator} from './sandbox/pronunciation-generator.js'; +import {createPronunciationDownstepPosition, createPronunciationGraph, createPronunciationText} from './sandbox/pronunciation-generator.js'; import {StructuredContentGenerator} from './sandbox/structured-content-generator.js'; export class DisplayGenerator { @@ -38,8 +38,6 @@ export class DisplayGenerator { this._templates = new HtmlTemplateCollection(); /** @type {StructuredContentGenerator} */ this._structuredContentGenerator = new StructuredContentGenerator(this._contentManager, document); - /** @type {PronunciationGenerator} */ - this._pronunciationGenerator = new PronunciationGenerator(); } /** */ @@ -73,10 +71,10 @@ export class DisplayGenerator { const headwordTagsContainer = this._querySelector(node, '.headword-list-tag-list'); const {headwords, type, inflectionRuleChainCandidates, definitions, frequencies, pronunciations} = dictionaryEntry; - const groupedPronunciations = DictionaryDataUtil.getGroupedPronunciations(dictionaryEntry); + const groupedPronunciations = getGroupedPronunciations(dictionaryEntry); const pronunciationCount = groupedPronunciations.reduce((i, v) => i + v.pronunciations.length, 0); - const groupedFrequencies = DictionaryDataUtil.groupTermFrequencies(dictionaryEntry); - const termTags = DictionaryDataUtil.groupTermTags(dictionaryEntry); + const groupedFrequencies = groupTermFrequencies(dictionaryEntry); + const termTags = groupTermTags(dictionaryEntry); /** @type {Set<string>} */ const uniqueTerms = new Set(); @@ -166,7 +164,7 @@ export class DisplayGenerator { const dictionaryIndicesContainer = this._querySelector(node, '.kanji-dictionary-indices'); this._setTextContent(glyphContainer, dictionaryEntry.character, 'ja'); - const groupedFrequencies = DictionaryDataUtil.groupKanjiFrequencies(dictionaryEntry.frequencies); + const groupedFrequencies = groupKanjiFrequencies(dictionaryEntry.frequencies); const dictionaryTag = this._createDictionaryTag(dictionaryEntry.dictionary); @@ -334,7 +332,7 @@ export class DisplayGenerator { node.dataset.isPrimary = `${isPrimaryAny}`; node.dataset.readingIsSame = `${reading === term}`; - node.dataset.frequency = DictionaryDataUtil.getTermFrequency(tags); + node.dataset.frequency = getTermFrequency(tags); node.dataset.matchTypes = [...matchTypes].join(' '); node.dataset.matchSources = [...matchSources].join(' '); @@ -415,7 +413,7 @@ export class DisplayGenerator { */ _createTermDefinition(definition, dictionaryTag, headwords, uniqueTerms, uniqueReadings) { const {dictionary, tags, headwordIndices, entries} = definition; - const disambiguations = DictionaryDataUtil.getDisambiguations(headwords, headwordIndices, uniqueTerms, uniqueReadings); + const disambiguations = getDisambiguations(headwords, headwordIndices, uniqueTerms, uniqueReadings); const node = this._instantiate('definition-item'); @@ -742,15 +740,15 @@ export class DisplayGenerator { this._createPronunciationDisambiguations(n, exclusiveTerms, exclusiveReadings); n = this._querySelector(node, '.pronunciation-downstep-notation-container'); - n.appendChild(this._pronunciationGenerator.createPronunciationDownstepPosition(position)); + n.appendChild(createPronunciationDownstepPosition(position)); n = this._querySelector(node, '.pronunciation-text-container'); n.lang = 'ja'; - n.appendChild(this._pronunciationGenerator.createPronunciationText(morae, position, nasalPositions, devoicePositions)); + n.appendChild(createPronunciationText(morae, position, nasalPositions, devoicePositions)); n = this._querySelector(node, '.pronunciation-graph-container'); - n.appendChild(this._pronunciationGenerator.createPronunciationGraph(morae, position)); + n.appendChild(createPronunciationGraph(morae, position)); return node; } @@ -1040,7 +1038,7 @@ export class DisplayGenerator { */ _getPronunciationCategories(reading, termPronunciations, wordClasses, headwordIndex) { if (termPronunciations.length === 0) { return null; } - const isVerbOrAdjective = DictionaryDataUtil.isNonNounVerbOrAdjective(wordClasses); + const isVerbOrAdjective = isNonNounVerbOrAdjective(wordClasses); /** @type {Set<import('japanese-util').PitchCategory>} */ const categories = new Set(); for (const termPronunciation of termPronunciations) { diff --git a/ext/js/display/sandbox/pronunciation-generator.js b/ext/js/display/sandbox/pronunciation-generator.js index 45631e74..373ec830 100644 --- a/ext/js/display/sandbox/pronunciation-generator.js +++ b/ext/js/display/sandbox/pronunciation-generator.js @@ -18,221 +18,219 @@ import {getKanaDiacriticInfo, isMoraPitchHigh} from '../../language/japanese.js'; -export class PronunciationGenerator { - /** - * @param {string[]} morae - * @param {number} downstepPosition - * @param {number[]} nasalPositions - * @param {number[]} devoicePositions - * @returns {HTMLSpanElement} - */ - createPronunciationText(morae, downstepPosition, nasalPositions, devoicePositions) { - const nasalPositionsSet = nasalPositions.length > 0 ? new Set(nasalPositions) : null; - const devoicePositionsSet = devoicePositions.length > 0 ? new Set(devoicePositions) : null; - const container = document.createElement('span'); - container.className = 'pronunciation-text'; - for (let i = 0, ii = morae.length; i < ii; ++i) { - const i1 = i + 1; - const mora = morae[i]; - const highPitch = isMoraPitchHigh(i, downstepPosition); - const highPitchNext = isMoraPitchHigh(i1, downstepPosition); - const nasal = nasalPositionsSet !== null && nasalPositionsSet.has(i1); - const devoice = devoicePositionsSet !== null && devoicePositionsSet.has(i1); - - const n1 = document.createElement('span'); - n1.className = 'pronunciation-mora'; - n1.dataset.position = `${i}`; - n1.dataset.pitch = highPitch ? 'high' : 'low'; - n1.dataset.pitchNext = highPitchNext ? 'high' : 'low'; - - const characterNodes = []; - for (const character of mora) { - const n2 = document.createElement('span'); - n2.className = 'pronunciation-character'; - n2.textContent = character; - n1.appendChild(n2); - characterNodes.push(n2); - } - - if (devoice) { - n1.dataset.devoice = 'true'; - const n3 = document.createElement('span'); - n3.className = 'pronunciation-devoice-indicator'; - n1.appendChild(n3); - } - if (nasal && characterNodes.length > 0) { - n1.dataset.nasal = 'true'; - - const group = document.createElement('span'); - group.className = 'pronunciation-character-group'; +/** + * @param {string[]} morae + * @param {number} downstepPosition + * @param {number[]} nasalPositions + * @param {number[]} devoicePositions + * @returns {HTMLSpanElement} + */ +export function createPronunciationText(morae, downstepPosition, nasalPositions, devoicePositions) { + const nasalPositionsSet = nasalPositions.length > 0 ? new Set(nasalPositions) : null; + const devoicePositionsSet = devoicePositions.length > 0 ? new Set(devoicePositions) : null; + const container = document.createElement('span'); + container.className = 'pronunciation-text'; + for (let i = 0, ii = morae.length; i < ii; ++i) { + const i1 = i + 1; + const mora = morae[i]; + const highPitch = isMoraPitchHigh(i, downstepPosition); + const highPitchNext = isMoraPitchHigh(i1, downstepPosition); + const nasal = nasalPositionsSet !== null && nasalPositionsSet.has(i1); + const devoice = devoicePositionsSet !== null && devoicePositionsSet.has(i1); - const n2 = characterNodes[0]; - const character = /** @type {string} */ (n2.textContent); + const n1 = document.createElement('span'); + n1.className = 'pronunciation-mora'; + n1.dataset.position = `${i}`; + n1.dataset.pitch = highPitch ? 'high' : 'low'; + n1.dataset.pitchNext = highPitchNext ? 'high' : 'low'; + + const characterNodes = []; + for (const character of mora) { + const n2 = document.createElement('span'); + n2.className = 'pronunciation-character'; + n2.textContent = character; + n1.appendChild(n2); + characterNodes.push(n2); + } - const characterInfo = getKanaDiacriticInfo(character); - if (characterInfo !== null) { - n1.dataset.originalText = mora; - n2.dataset.originalText = character; - n2.textContent = characterInfo.character; - } + if (devoice) { + n1.dataset.devoice = 'true'; + const n3 = document.createElement('span'); + n3.className = 'pronunciation-devoice-indicator'; + n1.appendChild(n3); + } + if (nasal && characterNodes.length > 0) { + n1.dataset.nasal = 'true'; - let n3 = document.createElement('span'); - n3.className = 'pronunciation-nasal-diacritic'; - n3.textContent = '\u309a'; // Combining handakuten - group.appendChild(n3); + const group = document.createElement('span'); + group.className = 'pronunciation-character-group'; - n3 = document.createElement('span'); - n3.className = 'pronunciation-nasal-indicator'; - group.appendChild(n3); + const n2 = characterNodes[0]; + const character = /** @type {string} */ (n2.textContent); - /** @type {ParentNode} */ (n2.parentNode).replaceChild(group, n2); - group.insertBefore(n2, group.firstChild); + const characterInfo = getKanaDiacriticInfo(character); + if (characterInfo !== null) { + n1.dataset.originalText = mora; + n2.dataset.originalText = character; + n2.textContent = characterInfo.character; } - const line = document.createElement('span'); - line.className = 'pronunciation-mora-line'; - n1.appendChild(line); + let n3 = document.createElement('span'); + n3.className = 'pronunciation-nasal-diacritic'; + n3.textContent = '\u309a'; // Combining handakuten + group.appendChild(n3); - container.appendChild(n1); - } - return container; - } + n3 = document.createElement('span'); + n3.className = 'pronunciation-nasal-indicator'; + group.appendChild(n3); - /** - * @param {string[]} morae - * @param {number} downstepPosition - * @returns {SVGSVGElement} - */ - createPronunciationGraph(morae, downstepPosition) { - const ii = morae.length; - - const svgns = 'http://www.w3.org/2000/svg'; - const svg = document.createElementNS(svgns, 'svg'); - svg.setAttribute('xmlns', svgns); - svg.setAttribute('class', 'pronunciation-graph'); - svg.setAttribute('focusable', 'false'); - svg.setAttribute('viewBox', `0 0 ${50 * (ii + 1)} 100`); - - if (ii <= 0) { return svg; } - - const path1 = document.createElementNS(svgns, 'path'); - svg.appendChild(path1); - - const path2 = document.createElementNS(svgns, 'path'); - svg.appendChild(path2); - - const pathPoints = []; - for (let i = 0; i < ii; ++i) { - const highPitch = isMoraPitchHigh(i, downstepPosition); - const highPitchNext = isMoraPitchHigh(i + 1, downstepPosition); - const x = i * 50 + 25; - const y = highPitch ? 25 : 75; - if (highPitch && !highPitchNext) { - this._addGraphDotDownstep(svg, svgns, x, y); - } else { - this._addGraphDot(svg, svgns, x, y); - } - pathPoints.push(`${x} ${y}`); + /** @type {ParentNode} */ (n2.parentNode).replaceChild(group, n2); + group.insertBefore(n2, group.firstChild); } - path1.setAttribute('class', 'pronunciation-graph-line'); - path1.setAttribute('d', `M${pathPoints.join(' L')}`); + const line = document.createElement('span'); + line.className = 'pronunciation-mora-line'; + n1.appendChild(line); + + container.appendChild(n1); + } + return container; +} - pathPoints.splice(0, ii - 1); - { - const highPitch = isMoraPitchHigh(ii, downstepPosition); - const x = ii * 50 + 25; - const y = highPitch ? 25 : 75; - this._addGraphTriangle(svg, svgns, x, y); - pathPoints.push(`${x} ${y}`); +/** + * @param {string[]} morae + * @param {number} downstepPosition + * @returns {SVGSVGElement} + */ +export function createPronunciationGraph(morae, downstepPosition) { + const ii = morae.length; + + const svgns = 'http://www.w3.org/2000/svg'; + const svg = document.createElementNS(svgns, 'svg'); + svg.setAttribute('xmlns', svgns); + svg.setAttribute('class', 'pronunciation-graph'); + svg.setAttribute('focusable', 'false'); + svg.setAttribute('viewBox', `0 0 ${50 * (ii + 1)} 100`); + + if (ii <= 0) { return svg; } + + const path1 = document.createElementNS(svgns, 'path'); + svg.appendChild(path1); + + const path2 = document.createElementNS(svgns, 'path'); + svg.appendChild(path2); + + const pathPoints = []; + for (let i = 0; i < ii; ++i) { + const highPitch = isMoraPitchHigh(i, downstepPosition); + const highPitchNext = isMoraPitchHigh(i + 1, downstepPosition); + const x = i * 50 + 25; + const y = highPitch ? 25 : 75; + if (highPitch && !highPitchNext) { + addGraphDotDownstep(svg, svgns, x, y); + } else { + addGraphDot(svg, svgns, x, y); } + pathPoints.push(`${x} ${y}`); + } - path2.setAttribute('class', 'pronunciation-graph-line-tail'); - path2.setAttribute('d', `M${pathPoints.join(' L')}`); + path1.setAttribute('class', 'pronunciation-graph-line'); + path1.setAttribute('d', `M${pathPoints.join(' L')}`); - return svg; + pathPoints.splice(0, ii - 1); + { + const highPitch = isMoraPitchHigh(ii, downstepPosition); + const x = ii * 50 + 25; + const y = highPitch ? 25 : 75; + addGraphTriangle(svg, svgns, x, y); + pathPoints.push(`${x} ${y}`); } - /** - * @param {number} downstepPosition - * @returns {HTMLSpanElement} - */ - createPronunciationDownstepPosition(downstepPosition) { - const downstepPositionString = `${downstepPosition}`; + path2.setAttribute('class', 'pronunciation-graph-line-tail'); + path2.setAttribute('d', `M${pathPoints.join(' L')}`); - const n1 = document.createElement('span'); - n1.className = 'pronunciation-downstep-notation'; - n1.dataset.downstepPosition = downstepPositionString; + return svg; +} - let n2 = document.createElement('span'); - n2.className = 'pronunciation-downstep-notation-prefix'; - n2.textContent = '['; - n1.appendChild(n2); +/** + * @param {number} downstepPosition + * @returns {HTMLSpanElement} + */ +export function createPronunciationDownstepPosition(downstepPosition) { + const downstepPositionString = `${downstepPosition}`; - n2 = document.createElement('span'); - n2.className = 'pronunciation-downstep-notation-number'; - n2.textContent = downstepPositionString; - n1.appendChild(n2); + const n1 = document.createElement('span'); + n1.className = 'pronunciation-downstep-notation'; + n1.dataset.downstepPosition = downstepPositionString; - n2 = document.createElement('span'); - n2.className = 'pronunciation-downstep-notation-suffix'; - n2.textContent = ']'; - n1.appendChild(n2); + let n2 = document.createElement('span'); + n2.className = 'pronunciation-downstep-notation-prefix'; + n2.textContent = '['; + n1.appendChild(n2); - return n1; - } + n2 = document.createElement('span'); + n2.className = 'pronunciation-downstep-notation-number'; + n2.textContent = downstepPositionString; + n1.appendChild(n2); - // Private + n2 = document.createElement('span'); + n2.className = 'pronunciation-downstep-notation-suffix'; + n2.textContent = ']'; + n1.appendChild(n2); - /** - * @param {Element} container - * @param {string} svgns - * @param {number} x - * @param {number} y - */ - _addGraphDot(container, svgns, x, y) { - container.appendChild(this._createGraphCircle(svgns, 'pronunciation-graph-dot', x, y, '15')); - } + return n1; +} - /** - * @param {Element} container - * @param {string} svgns - * @param {number} x - * @param {number} y - */ - _addGraphDotDownstep(container, svgns, x, y) { - container.appendChild(this._createGraphCircle(svgns, 'pronunciation-graph-dot-downstep1', x, y, '15')); - container.appendChild(this._createGraphCircle(svgns, 'pronunciation-graph-dot-downstep2', x, y, '5')); - } +// Private - /** - * @param {Element} container - * @param {string} svgns - * @param {number} x - * @param {number} y - */ - _addGraphTriangle(container, svgns, x, y) { - const node = document.createElementNS(svgns, 'path'); - node.setAttribute('class', 'pronunciation-graph-triangle'); - node.setAttribute('d', 'M0 13 L15 -13 L-15 -13 Z'); - node.setAttribute('transform', `translate(${x},${y})`); - container.appendChild(node); - } +/** + * @param {Element} container + * @param {string} svgns + * @param {number} x + * @param {number} y + */ +function addGraphDot(container, svgns, x, y) { + container.appendChild(createGraphCircle(svgns, 'pronunciation-graph-dot', x, y, '15')); +} - /** - * @param {string} svgns - * @param {string} className - * @param {number} x - * @param {number} y - * @param {string} radius - * @returns {Element} - */ - _createGraphCircle(svgns, className, x, y, radius) { - const node = document.createElementNS(svgns, 'circle'); - node.setAttribute('class', className); - node.setAttribute('cx', `${x}`); - node.setAttribute('cy', `${y}`); - node.setAttribute('r', radius); - return node; - } +/** + * @param {Element} container + * @param {string} svgns + * @param {number} x + * @param {number} y + */ +function addGraphDotDownstep(container, svgns, x, y) { + container.appendChild(createGraphCircle(svgns, 'pronunciation-graph-dot-downstep1', x, y, '15')); + container.appendChild(createGraphCircle(svgns, 'pronunciation-graph-dot-downstep2', x, y, '5')); +} + +/** + * @param {Element} container + * @param {string} svgns + * @param {number} x + * @param {number} y + */ +function addGraphTriangle(container, svgns, x, y) { + const node = document.createElementNS(svgns, 'path'); + node.setAttribute('class', 'pronunciation-graph-triangle'); + node.setAttribute('d', 'M0 13 L15 -13 L-15 -13 Z'); + node.setAttribute('transform', `translate(${x},${y})`); + container.appendChild(node); +} + +/** + * @param {string} svgns + * @param {string} className + * @param {number} x + * @param {number} y + * @param {string} radius + * @returns {Element} + */ +function createGraphCircle(svgns, className, x, y, radius) { + const node = document.createElementNS(svgns, 'circle'); + node.setAttribute('class', className); + node.setAttribute('cx', `${x}`); + node.setAttribute('cy', `${y}`); + node.setAttribute('r', radius); + return node; } diff --git a/ext/js/dom/dom-text-scanner.js b/ext/js/dom/dom-text-scanner.js index d605aeca..5caa32f7 100644 --- a/ext/js/dom/dom-text-scanner.js +++ b/ext/js/dom/dom-text-scanner.js @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {StringUtil} from '../data/sandbox/string-util.js'; +import {readCodePointsBackward, readCodePointsForward} from '../data/sandbox/string-util.js'; /** * A class used to scan text in a document. @@ -172,7 +172,7 @@ export class DOMTextScanner { if (resetOffset) { this._offset = 0; } while (this._offset < nodeValueLength) { - const char = StringUtil.readCodePointsForward(nodeValue, this._offset, 1); + const char = readCodePointsForward(nodeValue, this._offset, 1); this._offset += char.length; const charAttributes = DOMTextScanner.getCharacterAttributes(char, preserveNewlines, preserveWhitespace); if (this._checkCharacterForward(char, charAttributes)) { break; } @@ -201,7 +201,7 @@ export class DOMTextScanner { if (resetOffset) { this._offset = nodeValueLength; } while (this._offset > 0) { - const char = StringUtil.readCodePointsBackward(nodeValue, this._offset - 1, 1); + const char = readCodePointsBackward(nodeValue, this._offset - 1, 1); this._offset -= char.length; const charAttributes = DOMTextScanner.getCharacterAttributes(char, preserveNewlines, preserveWhitespace); if (this._checkCharacterBackward(char, charAttributes)) { break; } diff --git a/ext/js/dom/text-source-element.js b/ext/js/dom/text-source-element.js index 477295e6..8727a4e1 100644 --- a/ext/js/dom/text-source-element.js +++ b/ext/js/dom/text-source-element.js @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {StringUtil} from '../data/sandbox/string-util.js'; +import {readCodePointsBackward, readCodePointsForward} from '../data/sandbox/string-util.js'; import {DocumentUtil} from './document-util.js'; /** @@ -118,7 +118,7 @@ export class TextSourceElement { const offset = fromEnd ? this._endOffset : this._startOffset; length = Math.min(this._fullContent.length - offset, length); if (length > 0) { - length = StringUtil.readCodePointsForward(this._fullContent, offset, length).length; + length = readCodePointsForward(this._fullContent, offset, length).length; } this._endOffset = offset + length; this._content = this._fullContent.substring(this._startOffset, this._endOffset); @@ -133,7 +133,7 @@ export class TextSourceElement { setStartOffset(length) { length = Math.min(this._startOffset, length); if (length > 0) { - length = StringUtil.readCodePointsBackward(this._fullContent, this._startOffset - 1, length).length; + length = readCodePointsBackward(this._fullContent, this._startOffset - 1, length).length; } this._startOffset -= length; this._content = this._fullContent.substring(this._startOffset, this._endOffset); diff --git a/ext/js/general/regex-util.js b/ext/js/general/regex-util.js index 301b1fcf..f6eca3b6 100644 --- a/ext/js/general/regex-util.js +++ b/ext/js/general/regex-util.js @@ -16,82 +16,76 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +/** @type {RegExp} @readonly */ +const matchReplacementPattern = /\$(?:\$|&|`|'|(\d\d?)|<([^>]*)>)/g; /** - * This class provides some general utility functions for regular expressions. + * Applies string.replace using a regular expression and replacement string as arguments. + * A source map of the changes is also maintained. + * @param {string} text A string of the text to replace. + * @param {import('./text-source-map.js').TextSourceMap} sourceMap An instance of `TextSourceMap` which corresponds to `text`. + * @param {RegExp} pattern A regular expression to use as the replacement. + * @param {string} replacement A replacement string that follows the format of the standard + * JavaScript regular expression replacement string. + * @returns {string} A new string with the pattern replacements applied and the source map updated. */ -export class RegexUtil { - /** @type {RegExp} @readonly */ - static _matchReplacementPattern = /\$(?:\$|&|`|'|(\d\d?)|<([^>]*)>)/g; +export function applyTextReplacement(text, sourceMap, pattern, replacement) { + const isGlobal = pattern.global; + if (isGlobal) { pattern.lastIndex = 0; } + for (let loop = true; loop; loop = isGlobal) { + const match = pattern.exec(text); + if (match === null) { break; } - /** - * Applies string.replace using a regular expression and replacement string as arguments. - * A source map of the changes is also maintained. - * @param {string} text A string of the text to replace. - * @param {import('./text-source-map.js').TextSourceMap} sourceMap An instance of `TextSourceMap` which corresponds to `text`. - * @param {RegExp} pattern A regular expression to use as the replacement. - * @param {string} replacement A replacement string that follows the format of the standard - * JavaScript regular expression replacement string. - * @returns {string} A new string with the pattern replacements applied and the source map updated. - */ - static applyTextReplacement(text, sourceMap, pattern, replacement) { - const isGlobal = pattern.global; - if (isGlobal) { pattern.lastIndex = 0; } - for (let loop = true; loop; loop = isGlobal) { - const match = pattern.exec(text); - if (match === null) { break; } + const matchText = match[0]; + const index = match.index; + const actualReplacement = applyMatchReplacement(replacement, match); + const actualReplacementLength = actualReplacement.length; + const delta = actualReplacementLength - (matchText.length > 0 ? matchText.length : -1); - const matchText = match[0]; - const index = match.index; - const actualReplacement = this.applyMatchReplacement(replacement, match); - const actualReplacementLength = actualReplacement.length; - const delta = actualReplacementLength - (matchText.length > 0 ? matchText.length : -1); + text = `${text.substring(0, index)}${actualReplacement}${text.substring(index + matchText.length)}`; + pattern.lastIndex += delta; - text = `${text.substring(0, index)}${actualReplacement}${text.substring(index + matchText.length)}`; - pattern.lastIndex += delta; - - if (actualReplacementLength > 0) { - sourceMap.insert(index, ...(new Array(actualReplacementLength).fill(0))); - sourceMap.combine(index - 1 + actualReplacementLength, matchText.length); - } else { - sourceMap.combine(index, matchText.length); - } + if (actualReplacementLength > 0) { + sourceMap.insert(index, ...(new Array(actualReplacementLength).fill(0))); + sourceMap.combine(index - 1 + actualReplacementLength, matchText.length); + } else { + sourceMap.combine(index, matchText.length); } - return text; } + return text; +} - /** - * Applies the replacement string for a given regular expression match. - * @param {string} replacement The replacement string that follows the format of the standard - * JavaScript regular expression replacement string. - * @param {RegExpMatchArray} match A match object returned from RegExp.match. - * @returns {string} A new string with the pattern replacement applied. - */ - static applyMatchReplacement(replacement, match) { - const pattern = this._matchReplacementPattern; - pattern.lastIndex = 0; - return replacement.replace(pattern, (g0, g1, g2) => { - if (typeof g1 !== 'undefined') { - const matchIndex = Number.parseInt(g1, 10); - if (matchIndex >= 1 && matchIndex <= match.length) { - return match[matchIndex]; - } - } else if (typeof g2 !== 'undefined') { - const {groups} = match; - if (typeof groups === 'object' && groups !== null && Object.prototype.hasOwnProperty.call(groups, g2)) { - return groups[g2]; - } - } else { - let {index} = match; - if (typeof index !== 'number') { index = 0; } - switch (g0) { - case '$': return '$'; - case '&': return match[0]; - case '`': return replacement.substring(0, index); - case '\'': return replacement.substring(index + g0.length); - } +/** + * Applies the replacement string for a given regular expression match. + * @param {string} replacement The replacement string that follows the format of the standard + * JavaScript regular expression replacement string. + * @param {RegExpMatchArray} match A match object returned from RegExp.match. + * @returns {string} A new string with the pattern replacement applied. + */ +export function applyMatchReplacement(replacement, match) { + const pattern = matchReplacementPattern; + pattern.lastIndex = 0; + return replacement.replace(pattern, (g0, g1, g2) => { + if (typeof g1 !== 'undefined') { + const matchIndex = Number.parseInt(g1, 10); + if (matchIndex >= 1 && matchIndex <= match.length) { + return match[matchIndex]; } - return g0; - }); - } + } else if (typeof g2 !== 'undefined') { + const {groups} = match; + if (typeof groups === 'object' && groups !== null && Object.prototype.hasOwnProperty.call(groups, g2)) { + return groups[g2]; + } + } else { + let {index} = match; + if (typeof index !== 'number') { index = 0; } + switch (g0) { + case '$': return '$'; + case '&': return match[0]; + case '`': return replacement.substring(0, index); + case '\'': return replacement.substring(index + g0.length); + } + } + return g0; + }); } diff --git a/ext/js/language/translator.js b/ext/js/language/translator.js index 9d2f18e0..334eb5b7 100644 --- a/ext/js/language/translator.js +++ b/ext/js/language/translator.js @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {RegexUtil} from '../general/regex-util.js'; +import {applyTextReplacement} from '../general/regex-util.js'; import {TextSourceMap} from '../general/text-source-map.js'; import {convertAlphabeticToKana} from './japanese-wanakana.js'; import {collapseEmphaticSequences, convertHalfWidthKanaToFullWidth, convertHiraganaToKatakana, convertKatakanaToHiragana, convertNumericToFullWidth, isCodePointJapanese} from './japanese.js'; @@ -506,7 +506,7 @@ export class Translator { */ _applyTextReplacements(text, sourceMap, replacements) { for (const {pattern, replacement} of replacements) { - text = RegexUtil.applyTextReplacement(text, sourceMap, pattern, replacement); + text = applyTextReplacement(text, sourceMap, pattern, replacement); } return text; } diff --git a/ext/js/media/audio-downloader.js b/ext/js/media/audio-downloader.js index b4f63b96..968c9353 100644 --- a/ext/js/media/audio-downloader.js +++ b/ext/js/media/audio-downloader.js @@ -20,7 +20,7 @@ import {RequestBuilder} from '../background/request-builder.js'; import {ExtensionError} from '../core/extension-error.js'; import {readResponseJson} from '../core/json.js'; import {JsonSchema} from '../data/json-schema.js'; -import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; +import {arrayBufferToBase64} from '../data/sandbox/array-buffer-util.js'; import {NativeSimpleDOMParser} from '../dom/native-simple-dom-parser.js'; import {SimpleDOMParser} from '../dom/simple-dom-parser.js'; import {isStringEntirelyKana} from '../language/japanese.js'; @@ -358,7 +358,7 @@ export class AudioDownloader { throw new Error('Could not retrieve audio'); } - const data = ArrayBufferUtil.arrayBufferToBase64(arrayBuffer); + const data = arrayBufferToBase64(arrayBuffer); const contentType = response.headers.get('Content-Type'); return {data, contentType}; } diff --git a/ext/js/media/media-util.js b/ext/js/media/media-util.js index 1f9f2d0b..3e492e42 100644 --- a/ext/js/media/media-util.js +++ b/ext/js/media/media-util.js @@ -17,119 +17,114 @@ */ /** - * MediaUtil is a class containing helper methods related to media processing. + * Gets the file extension of a file path. URL search queries and hash + * fragments are not handled. + * @param {string} path The path to the file. + * @returns {string} The file extension, including the '.', or an empty string + * if there is no file extension. */ -export class MediaUtil { - /** - * Gets the file extension of a file path. URL search queries and hash - * fragments are not handled. - * @param {string} path The path to the file. - * @returns {string} The file extension, including the '.', or an empty string - * if there is no file extension. - */ - static getFileNameExtension(path) { - const match = /\.[^./\\]*$/.exec(path); - return match !== null ? match[0] : ''; - } +export function getFileNameExtension(path) { + const match = /\.[^./\\]*$/.exec(path); + return match !== null ? match[0] : ''; +} - /** - * Gets an image file's media type using a file path. - * @param {string} path The path to the file. - * @returns {?string} The media type string if it can be determined from the file path, - * otherwise `null`. - */ - static getImageMediaTypeFromFileName(path) { - switch (this.getFileNameExtension(path).toLowerCase()) { - case '.apng': - return 'image/apng'; - case '.bmp': - return 'image/bmp'; - case '.gif': - return 'image/gif'; - case '.ico': - case '.cur': - return 'image/x-icon'; - case '.jpg': - case '.jpeg': - case '.jfif': - case '.pjpeg': - case '.pjp': - return 'image/jpeg'; - case '.png': - return 'image/png'; - case '.svg': - return 'image/svg+xml'; - case '.tif': - case '.tiff': - return 'image/tiff'; - case '.webp': - return 'image/webp'; - default: - return null; - } +/** + * Gets an image file's media type using a file path. + * @param {string} path The path to the file. + * @returns {?string} The media type string if it can be determined from the file path, + * otherwise `null`. + */ +export function getImageMediaTypeFromFileName(path) { + switch (getFileNameExtension(path).toLowerCase()) { + case '.apng': + return 'image/apng'; + case '.bmp': + return 'image/bmp'; + case '.gif': + return 'image/gif'; + case '.ico': + case '.cur': + return 'image/x-icon'; + case '.jpg': + case '.jpeg': + case '.jfif': + case '.pjpeg': + case '.pjp': + return 'image/jpeg'; + case '.png': + return 'image/png'; + case '.svg': + return 'image/svg+xml'; + case '.tif': + case '.tiff': + return 'image/tiff'; + case '.webp': + return 'image/webp'; + default: + return null; } +} - /** - * Gets the file extension for a corresponding media type. - * @param {string} mediaType The media type to use. - * @returns {?string} A file extension including the dot for the media type, - * otherwise `null`. - */ - static getFileExtensionFromImageMediaType(mediaType) { - switch (mediaType) { - case 'image/apng': - return '.apng'; - case 'image/bmp': - return '.bmp'; - case 'image/gif': - return '.gif'; - case 'image/x-icon': - return '.ico'; - case 'image/jpeg': - return '.jpeg'; - case 'image/png': - return '.png'; - case 'image/svg+xml': - return '.svg'; - case 'image/tiff': - return '.tiff'; - case 'image/webp': - return '.webp'; - default: - return null; - } +/** + * Gets the file extension for a corresponding media type. + * @param {string} mediaType The media type to use. + * @returns {?string} A file extension including the dot for the media type, + * otherwise `null`. + */ +export function getFileExtensionFromImageMediaType(mediaType) { + switch (mediaType) { + case 'image/apng': + return '.apng'; + case 'image/bmp': + return '.bmp'; + case 'image/gif': + return '.gif'; + case 'image/x-icon': + return '.ico'; + case 'image/jpeg': + return '.jpeg'; + case 'image/png': + return '.png'; + case 'image/svg+xml': + return '.svg'; + case 'image/tiff': + return '.tiff'; + case 'image/webp': + return '.webp'; + default: + return null; } +} - /** - * Gets the file extension for a corresponding media type. - * @param {string} mediaType The media type to use. - * @returns {?string} A file extension including the dot for the media type, - * otherwise `null`. - */ - static getFileExtensionFromAudioMediaType(mediaType) { - switch (mediaType) { - case 'audio/aac': - return '.aac'; - case 'audio/mpeg': - case 'audio/mp3': - return '.mp3'; - case 'audio/mp4': - return '.mp4'; - case 'audio/ogg': - case 'audio/vorbis': - return '.ogg'; - case 'audio/vnd.wav': - case 'audio/wave': - case 'audio/wav': - case 'audio/x-wav': - case 'audio/x-pn-wav': - return '.wav'; - case 'audio/flac': - return '.flac'; - case 'audio/webm': - return '.webm'; - default: - return null; - } +/** + * Gets the file extension for a corresponding media type. + * @param {string} mediaType The media type to use. + * @returns {?string} A file extension including the dot for the media type, + * otherwise `null`. + */ +export function getFileExtensionFromAudioMediaType(mediaType) { + switch (mediaType) { + case 'audio/aac': + return '.aac'; + case 'audio/mpeg': + case 'audio/mp3': + return '.mp3'; + case 'audio/mp4': + return '.mp4'; + case 'audio/ogg': + case 'audio/vorbis': + return '.ogg'; + case 'audio/vnd.wav': + case 'audio/wave': + case 'audio/wav': + case 'audio/x-wav': + case 'audio/x-pn-wav': + return '.wav'; + case 'audio/flac': + return '.flac'; + case 'audio/webm': + return '.webm'; + default: + return null; } } diff --git a/ext/js/pages/action-popup-main.js b/ext/js/pages/action-popup-main.js index b978d989..86201e83 100644 --- a/ext/js/pages/action-popup-main.js +++ b/ext/js/pages/action-popup-main.js @@ -16,7 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {PermissionsUtil} from '../data/permissions-util.js'; +import {getAllPermissions, hasRequiredPermissionsForOptions} from '../data/permissions-util.js'; import {querySelectorNotNull} from '../dom/query-selector.js'; import {HotkeyHelpController} from '../input/hotkey-help-controller.js'; import {yomitan} from '../yomitan.js'; @@ -25,8 +25,6 @@ class DisplayController { constructor() { /** @type {?import('settings').Options} */ this._optionsFull = null; - /** @type {PermissionsUtil} */ - this._permissionsUtil = new PermissionsUtil(); } /** */ @@ -286,8 +284,8 @@ class DisplayController { * @param {import('settings').ProfileOptions} options */ async _updatePermissionsWarnings(options) { - const permissions = await this._permissionsUtil.getAllPermissions(); - if (this._permissionsUtil.hasRequiredPermissionsForOptions(permissions, options)) { return; } + const permissions = await getAllPermissions(); + if (hasRequiredPermissionsForOptions(permissions, options)) { return; } const warnings = /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.action-open-permissions,.permissions-required-warning')); for (const node of warnings) { diff --git a/ext/js/pages/settings/anki-controller.js b/ext/js/pages/settings/anki-controller.js index d1faf491..09ab3c03 100644 --- a/ext/js/pages/settings/anki-controller.js +++ b/ext/js/pages/settings/anki-controller.js @@ -21,7 +21,8 @@ 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 {stringContainsAnyFieldMarker} from '../../data/anki-util.js'; +import {getRequiredPermissionsForAnkiFieldValue, hasPermissions, setPermissionsGranted} from '../../data/permissions-util.js'; import {querySelectorNotNull} from '../../dom/query-selector.js'; import {SelectorObserver} from '../../dom/selector-observer.js'; import {ObjectPropertyAccessor} from '../../general/object-property-accessor.js'; @@ -211,7 +212,7 @@ export class AnkiController { * @returns {string[]} */ getRequiredPermissions(fieldValue) { - return this._settingsController.permissionsUtil.getRequiredPermissionsForAnkiFieldValue(fieldValue); + return getRequiredPermissionsForAnkiFieldValue(fieldValue); } // Private @@ -738,7 +739,7 @@ class AnkiCardController { */ _validateField(node, index) { let valid = (node.dataset.hasPermissions !== 'false'); - if (valid && index === 0 && !AnkiUtil.stringContainsAnyFieldMarker(node.value)) { + if (valid && index === 0 && !stringContainsAnyFieldMarker(node.value)) { valid = false; } node.dataset.invalid = `${!valid}`; @@ -936,7 +937,7 @@ class AnkiCardController { */ async _requestPermissions(permissions) { try { - await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true); + await setPermissionsGranted({permissions}, true); } catch (e) { log.error(e); } @@ -952,12 +953,12 @@ class AnkiCardController { const permissions = this._ankiController.getRequiredPermissions(fieldValue); if (permissions.length > 0) { node.dataset.requiredPermission = permissions.join(' '); - const hasPermissions = await ( + const hasPermissions2 = await ( request ? - this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, true) : - this._settingsController.permissionsUtil.hasPermissions({permissions}) + setPermissionsGranted({permissions}, true) : + hasPermissions({permissions}) ); - node.dataset.hasPermissions = `${hasPermissions}`; + node.dataset.hasPermissions = `${hasPermissions2}`; } else { delete node.dataset.requiredPermission; delete node.dataset.hasPermissions; @@ -977,15 +978,15 @@ class AnkiCardController { if (typeof requiredPermission !== 'string') { continue; } const requiredPermissionArray = (requiredPermission.length === 0 ? [] : requiredPermission.split(' ')); - let hasPermissions = true; + let hasPermissions2 = true; for (const permission of requiredPermissionArray) { if (!permissionsSet.has(permission)) { - hasPermissions = false; + hasPermissions2 = false; break; } } - inputField.dataset.hasPermissions = `${hasPermissions}`; + inputField.dataset.hasPermissions = `${hasPermissions2}`; this._validateField(inputField, i); } } diff --git a/ext/js/pages/settings/backup-controller.js b/ext/js/pages/settings/backup-controller.js index f2eccd1e..053cc96b 100644 --- a/ext/js/pages/settings/backup-controller.js +++ b/ext/js/pages/settings/backup-controller.js @@ -22,7 +22,8 @@ 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 {getAllPermissions} from '../../data/permissions-util.js'; +import {arrayBufferUtf8Decode} from '../../data/sandbox/array-buffer-util.js'; import {querySelectorNotNull} from '../../dom/query-selector.js'; import {yomitan} from '../../yomitan.js'; import {DictionaryController} from './dictionary-controller.js'; @@ -135,7 +136,7 @@ export class BackupController { const optionsFull = await this._settingsController.getOptionsFull(); const environment = await yomitan.api.getEnvironmentInfo(); const fieldTemplatesDefault = await yomitan.api.getDefaultAnkiFieldTemplates(); - const permissions = await this._settingsController.permissionsUtil.getAllPermissions(); + const permissions = await getAllPermissions(); // Format options for (const {options} of optionsFull.profiles) { @@ -425,7 +426,7 @@ export class BackupController { async _importSettingsFile(file) { if (this._optionsUtil === null) { throw new Error('OptionsUtil invalid'); } - const dataString = ArrayBufferUtil.arrayBufferUtf8Decode(await this._readFileArrayBuffer(file)); + const dataString = arrayBufferUtf8Decode(await this._readFileArrayBuffer(file)); /** @type {import('backup-controller').BackupData} */ const data = parseJson(dataString); diff --git a/ext/js/pages/settings/permissions-origin-controller.js b/ext/js/pages/settings/permissions-origin-controller.js index a0f23af6..9447e3cc 100644 --- a/ext/js/pages/settings/permissions-origin-controller.js +++ b/ext/js/pages/settings/permissions-origin-controller.js @@ -18,6 +18,7 @@ import {EventListenerCollection} from '../../core/event-listener-collection.js'; import {toError} from '../../core/to-error.js'; +import {getAllPermissions, setPermissionsGranted} from '../../data/permissions-util.js'; import {querySelectorNotNull} from '../../dom/query-selector.js'; export class PermissionsOriginController { @@ -140,7 +141,7 @@ export class PermissionsOriginController { /** */ async _updatePermissions() { - const permissions = await this._settingsController.permissionsUtil.getAllPermissions(); + const permissions = await getAllPermissions(); this._onPermissionsChanged({permissions}); } @@ -152,7 +153,7 @@ export class PermissionsOriginController { async _setOriginPermissionEnabled(origin, enabled) { let added = false; try { - added = await this._settingsController.permissionsUtil.setPermissionsGranted({origins: [origin]}, enabled); + added = await setPermissionsGranted({origins: [origin]}, enabled); } catch (e) { const errorContainer = /** @type {HTMLElement} */ (this._errorContainer); errorContainer.hidden = false; diff --git a/ext/js/pages/settings/permissions-toggle-controller.js b/ext/js/pages/settings/permissions-toggle-controller.js index c775aa12..25204dce 100644 --- a/ext/js/pages/settings/permissions-toggle-controller.js +++ b/ext/js/pages/settings/permissions-toggle-controller.js @@ -16,6 +16,7 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ +import {getAllPermissions, hasPermissions, setPermissionsGranted} from '../../data/permissions-util.js'; import {ObjectPropertyAccessor} from '../../general/object-property-accessor.js'; export class PermissionsToggleController { @@ -85,11 +86,11 @@ export class PermissionsToggleController { toggle.checked = valuePre; const permissions = this._getRequiredPermissions(toggle); try { - value = await this._settingsController.permissionsUtil.setPermissionsGranted({permissions}, value); + value = await setPermissionsGranted({permissions}, value); } catch (error) { value = valuePre; try { - value = await this._settingsController.permissionsUtil.hasPermissions({permissions}); + value = await hasPermissions({permissions}); } catch (error2) { // NOP } @@ -111,13 +112,13 @@ export class PermissionsToggleController { const permissionsSet = new Set(typeof permissions2 !== 'undefined' ? permissions2 : []); for (const toggle of /** @type {NodeListOf<HTMLInputElement>} */ (this._toggles)) { const {permissionsSetting} = toggle.dataset; - const hasPermissions = this._hasAll(permissionsSet, this._getRequiredPermissions(toggle)); + const hasPermissions2 = this._hasAll(permissionsSet, this._getRequiredPermissions(toggle)); if (typeof permissionsSetting === 'string') { - const valid = !toggle.checked || hasPermissions; + const valid = !toggle.checked || hasPermissions2; this._setToggleValid(toggle, valid); } else { - toggle.checked = hasPermissions; + toggle.checked = hasPermissions2; } } } @@ -134,7 +135,7 @@ export class PermissionsToggleController { /** */ async _updateValidity() { - const permissions = await this._settingsController.permissionsUtil.getAllPermissions(); + const permissions = await getAllPermissions(); this._onPermissionsChanged({permissions}); } diff --git a/ext/js/pages/settings/recommended-permissions-controller.js b/ext/js/pages/settings/recommended-permissions-controller.js index 84a4ef10..b7bb1ea8 100644 --- a/ext/js/pages/settings/recommended-permissions-controller.js +++ b/ext/js/pages/settings/recommended-permissions-controller.js @@ -18,6 +18,7 @@ import {EventListenerCollection} from '../../core/event-listener-collection.js'; import {toError} from '../../core/to-error.js'; +import {getAllPermissions, setPermissionsGranted} from '../../data/permissions-util.js'; export class RecommendedPermissionsController { /** @@ -77,7 +78,7 @@ export class RecommendedPermissionsController { /** */ async _updatePermissions() { - const permissions = await this._settingsController.permissionsUtil.getAllPermissions(); + const permissions = await getAllPermissions(); this._onPermissionsChanged({permissions}); } @@ -89,7 +90,7 @@ export class RecommendedPermissionsController { async _setOriginPermissionEnabled(origin, enabled) { let added = false; try { - added = await this._settingsController.permissionsUtil.setPermissionsGranted({origins: [origin]}, enabled); + added = await setPermissionsGranted({origins: [origin]}, enabled); } catch (e) { if (this._errorContainer !== null) { this._errorContainer.hidden = false; diff --git a/ext/js/pages/settings/settings-controller.js b/ext/js/pages/settings/settings-controller.js index 25f5e8ad..49fa9c9e 100644 --- a/ext/js/pages/settings/settings-controller.js +++ b/ext/js/pages/settings/settings-controller.js @@ -20,7 +20,7 @@ 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 {getAllPermissions} from '../../data/permissions-util.js'; import {HtmlTemplateCollection} from '../../dom/html-template-collection.js'; import {yomitan} from '../../yomitan.js'; @@ -41,8 +41,6 @@ export class SettingsController extends EventDispatcher { /** @type {HtmlTemplateCollection} */ this._templates = new HtmlTemplateCollection(); this._templates.load(document); - /** @type {PermissionsUtil} */ - this._permissionsUtil = new PermissionsUtil(); } /** @type {string} */ @@ -60,11 +58,6 @@ export class SettingsController extends EventDispatcher { this._setProfileIndex(value, true); } - /** @type {PermissionsUtil} */ - get permissionsUtil() { - return this._permissionsUtil; - } - /** */ async prepare() { yomitan.on('optionsUpdated', this._onOptionsUpdated.bind(this)); @@ -338,7 +331,7 @@ export class SettingsController extends EventDispatcher { const eventName = 'permissionsChanged'; if (!this.hasListeners(eventName)) { return; } - const permissions = await this._permissionsUtil.getAllPermissions(); + const permissions = await getAllPermissions(); this.trigger(eventName, {permissions}); } diff --git a/ext/js/templates/sandbox/anki-template-renderer-content-manager.js b/ext/js/templates/sandbox/anki-template-renderer-content-manager.js index 932b6ab7..664746bf 100644 --- a/ext/js/templates/sandbox/anki-template-renderer-content-manager.js +++ b/ext/js/templates/sandbox/anki-template-renderer-content-manager.js @@ -24,7 +24,6 @@ export class AnkiTemplateRendererContentManager { * Creates a new instance of the class. * @param {import('./template-renderer-media-provider.js').TemplateRendererMediaProvider} mediaProvider The media provider for the object. * @param {import('anki-templates').NoteData} data The data object passed to the Handlebars template renderer. - * See AnkiNoteDataCreator.create's return value for structure information. */ constructor(mediaProvider, data) { /** @type {import('./template-renderer-media-provider.js').TemplateRendererMediaProvider} */ diff --git a/ext/js/templates/sandbox/anki-template-renderer.js b/ext/js/templates/sandbox/anki-template-renderer.js index 52087336..26d3f336 100644 --- a/ext/js/templates/sandbox/anki-template-renderer.js +++ b/ext/js/templates/sandbox/anki-template-renderer.js @@ -17,9 +17,9 @@ */ import {Handlebars} from '../../../lib/handlebars.js'; -import {AnkiNoteDataCreator} from '../../data/sandbox/anki-note-data-creator.js'; -import {DictionaryDataUtil} from '../../dictionary/dictionary-data-util.js'; -import {PronunciationGenerator} from '../../display/sandbox/pronunciation-generator.js'; +import {createAnkiNoteData} from '../../data/sandbox/anki-note-data-creator.js'; +import {getPronunciationsOfType, isNonNounVerbOrAdjective} from '../../dictionary/dictionary-data-util.js'; +import {createPronunciationDownstepPosition, createPronunciationGraph, createPronunciationText} from '../../display/sandbox/pronunciation-generator.js'; import {StructuredContentGenerator} from '../../display/sandbox/structured-content-generator.js'; import {CssStyleApplier} from '../../dom/sandbox/css-style-applier.js'; import {convertHiraganaToKatakana, convertKatakanaToHiragana, distributeFurigana, getKanaMorae, getPitchCategory, isMoraPitchHigh} from '../../language/japanese.js'; @@ -44,12 +44,8 @@ export class AnkiTemplateRenderer { this._structuredContentDatasetKeyIgnorePattern = /^sc([^a-z]|$)/; /** @type {TemplateRenderer} */ this._templateRenderer = new TemplateRenderer(); - /** @type {AnkiNoteDataCreator} */ - this._ankiNoteDataCreator = new AnkiNoteDataCreator(); /** @type {TemplateRendererMediaProvider} */ this._mediaProvider = new TemplateRendererMediaProvider(); - /** @type {PronunciationGenerator} */ - this._pronunciationGenerator = new PronunciationGenerator(); /** @type {?(Map<string, unknown>[])} */ this._stateStack = null; /** @type {?import('anki-note-builder').Requirement[]} */ @@ -104,7 +100,7 @@ export class AnkiTemplateRenderer { ]); /* eslint-enable no-multi-spaces */ this._templateRenderer.registerDataType('ankiNote', { - modifier: ({marker, commonData}) => this._ankiNoteDataCreator.create(marker, commonData), + modifier: ({marker, commonData}) => createAnkiNoteData(marker, commonData), composeData: ({marker}, commonData) => ({marker, commonData}) }); this._templateRenderer.setRenderCallbacks( @@ -550,8 +546,8 @@ export class AnkiTemplateRenderer { const categories = new Set(); for (const {headwordIndex, pronunciations} of termPronunciations) { const {reading, wordClasses} = headwords[headwordIndex]; - const isVerbOrAdjective = DictionaryDataUtil.isNonNounVerbOrAdjective(wordClasses); - const pitches = DictionaryDataUtil.getPronunciationsOfType(pronunciations, 'pitch-accent'); + const isVerbOrAdjective = isNonNounVerbOrAdjective(wordClasses); + const pitches = getPronunciationsOfType(pronunciations, 'pitch-accent'); for (const {position} of pitches) { const category = getPitchCategory(reading, position, isVerbOrAdjective); if (category !== null) { @@ -737,11 +733,11 @@ export class AnkiTemplateRenderer { switch (format) { case 'text': - return this._getPronunciationHtml(this._pronunciationGenerator.createPronunciationText(morae, downstepPosition, nasalPositions, devoicePositions)); + return this._getPronunciationHtml(createPronunciationText(morae, downstepPosition, nasalPositions, devoicePositions)); case 'graph': - return this._getPronunciationHtml(this._pronunciationGenerator.createPronunciationGraph(morae, downstepPosition)); + return this._getPronunciationHtml(createPronunciationGraph(morae, downstepPosition)); case 'position': - return this._getPronunciationHtml(this._pronunciationGenerator.createPronunciationDownstepPosition(downstepPosition)); + return this._getPronunciationHtml(createPronunciationDownstepPosition(downstepPosition)); default: return ''; } diff --git a/test/dictionary-data.test.js b/test/dictionary-data.test.js index 438e1e97..9f8ba6f0 100644 --- a/test/dictionary-data.test.js +++ b/test/dictionary-data.test.js @@ -53,7 +53,10 @@ describe('Dictionary data', () => { expected3: expectedResults3[i] })); describe.each(testCases)('Test %#: $data.name', ({data, expected1, expected2, expected3}) => { - test('Test', async ({translator, ankiNoteDataCreator, expect}) => { + test('Test', async ({window, translator, expect}) => { + // The window property needs to be referenced for it to be initialized. + // It is needed for DOM access for structured content. + void window; switch (data.func) { case 'findTerms': { @@ -62,7 +65,7 @@ describe('Dictionary data', () => { const options = createFindOptions(dictionaryName, optionsPresets, data.options); const {dictionaryEntries, originalTextLength} = await translator.findTerms(mode, text, options); const renderResults = mode !== 'simple' ? await getTemplateRenderResults(dictionaryEntries, 'terms', mode, template, expect) : null; - const noteDataList = mode !== 'simple' ? dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, mode)) : null; + const noteDataList = mode !== 'simple' ? dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(dictionaryEntry, mode)) : null; expect.soft(originalTextLength).toStrictEqual(expected1.originalTextLength); expect.soft(dictionaryEntries).toStrictEqual(expected1.dictionaryEntries); expect.soft(noteDataList).toEqual(expected2.noteDataList); @@ -76,7 +79,7 @@ describe('Dictionary data', () => { const options = createFindOptions(dictionaryName, optionsPresets, data.options); const dictionaryEntries = await translator.findKanji(text, options); const renderResults = await getTemplateRenderResults(dictionaryEntries, 'kanji', 'split', template, expect); - const noteDataList = dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, 'split')); + const noteDataList = dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(dictionaryEntry, 'split')); expect.soft(dictionaryEntries).toStrictEqual(expected1.dictionaryEntries); expect.soft(noteDataList).toEqual(expected2.noteDataList); expect.soft(renderResults).toStrictEqual(expected3.results); diff --git a/test/dictionary-data.write.js b/test/dictionary-data.write.js index bdf635c8..d88bd3cd 100644 --- a/test/dictionary-data.write.js +++ b/test/dictionary-data.write.js @@ -35,7 +35,7 @@ const dirname = path.dirname(fileURLToPath(import.meta.url)); const dictionaryName = 'Test Dictionary 2'; const test = await createTranslatorTest(void 0, path.join(dirname, 'data/dictionaries/valid-dictionary1'), dictionaryName); -test('Write dictionary data expected data', async ({translator, ankiNoteDataCreator, expect}) => { +test('Write dictionary data expected data', async ({translator, expect}) => { const testInputsFilePath = path.join(dirname, 'data/translator-test-inputs.json'); /** @type {import('test/translator').TranslatorTestInputs} */ const {optionsPresets, tests} = parseJson(readFileSync(testInputsFilePath, {encoding: 'utf8'})); @@ -63,7 +63,7 @@ test('Write dictionary data expected data', async ({translator, ankiNoteDataCrea const options = createFindOptions(dictionaryName, optionsPresets, data.options); const {dictionaryEntries, originalTextLength} = await translator.findTerms(mode, text, options); const renderResults = mode !== 'simple' ? await getTemplateRenderResults(dictionaryEntries, 'terms', mode, template, null) : null; - const noteDataList = mode !== 'simple' ? dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, mode)) : null; + const noteDataList = mode !== 'simple' ? dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(dictionaryEntry, mode)) : null; actualResults1.push({name, originalTextLength, dictionaryEntries}); actualResults2.push({name, noteDataList}); actualResults3.push({name, results: renderResults}); @@ -76,7 +76,7 @@ test('Write dictionary data expected data', async ({translator, ankiNoteDataCrea const options = createFindOptions(dictionaryName, optionsPresets, data.options); const dictionaryEntries = await translator.findKanji(text, options); const renderResults = await getTemplateRenderResults(dictionaryEntries, 'kanji', 'split', template, null); - const noteDataList = dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, 'split')); + const noteDataList = dictionaryEntries.map((dictionaryEntry) => createTestAnkiNoteData(dictionaryEntry, 'split')); actualResults1.push({name, dictionaryEntries}); actualResults2.push({name, noteDataList}); actualResults3.push({name, results: renderResults}); diff --git a/test/fixtures/translator-test.js b/test/fixtures/translator-test.js index 58247b70..d1b3de8b 100644 --- a/test/fixtures/translator-test.js +++ b/test/fixtures/translator-test.js @@ -23,7 +23,6 @@ import {dirname, join} from 'path'; import {expect, vi} from 'vitest'; import {parseJson} from '../../dev/json.js'; import {createDictionaryArchive} from '../../dev/util.js'; -import {AnkiNoteDataCreator} from '../../ext/js/data/sandbox/anki-note-data-creator.js'; import {DictionaryDatabase} from '../../ext/js/dictionary/dictionary-database.js'; import {DictionaryImporter} from '../../ext/js/dictionary/dictionary-importer.js'; import {Translator} from '../../ext/js/language/translator.js'; @@ -42,7 +41,7 @@ vi.stubGlobal('chrome', chrome); /** * @param {string} dictionaryDirectory * @param {string} dictionaryName - * @returns {Promise<{translator: Translator, ankiNoteDataCreator: AnkiNoteDataCreator}>} + * @returns {Promise<Translator>} */ async function createTranslatorContext(dictionaryDirectory, dictionaryName) { // Dictionary @@ -69,31 +68,23 @@ async function createTranslatorContext(dictionaryDirectory, dictionaryName) { const deinflectionReasons = parseJson(readFileSync(languageTransformDescriptorPath, {encoding: 'utf8'})); translator.prepare(deinflectionReasons); - // Assign properties - const ankiNoteDataCreator = new AnkiNoteDataCreator(); - return {translator, ankiNoteDataCreator}; + return translator; } /** * @param {string|undefined} htmlFilePath * @param {string} dictionaryDirectory * @param {string} dictionaryName - * @returns {Promise<import('vitest').TestAPI<{window: import('jsdom').DOMWindow, translator: Translator, ankiNoteDataCreator: AnkiNoteDataCreator}>>} + * @returns {Promise<import('vitest').TestAPI<{window: import('jsdom').DOMWindow, translator: Translator}>>} */ export async function createTranslatorTest(htmlFilePath, dictionaryDirectory, dictionaryName) { const test = createDomTest(htmlFilePath); - const {translator, ankiNoteDataCreator} = await createTranslatorContext(dictionaryDirectory, dictionaryName); - /** @type {import('vitest').TestAPI<{window: import('jsdom').DOMWindow, translator: Translator, ankiNoteDataCreator: AnkiNoteDataCreator}>} */ + const translator = await createTranslatorContext(dictionaryDirectory, dictionaryName); + /** @type {import('vitest').TestAPI<{window: import('jsdom').DOMWindow, translator: Translator}>} */ const result = test.extend({ window: async ({window}, use) => { await use(window); }, // eslint-disable-next-line no-empty-pattern - translator: async ({}, use) => { await use(translator); }, - ankiNoteDataCreator: async ({window}, use) => { - // The window property needs to be referenced for it to be initialized. - // It is needed for DOM access for structured content. - void window; - await use(ankiNoteDataCreator); - } + translator: async ({}, use) => { await use(translator); } }); return result; } diff --git a/test/profile-conditions-util.test.js b/test/profile-conditions-util.test.js index 7af5f223..fcd53939 100644 --- a/test/profile-conditions-util.test.js +++ b/test/profile-conditions-util.test.js @@ -17,7 +17,7 @@ */ import {describe, expect, test} from 'vitest'; -import {ProfileConditionsUtil} from '../ext/js/background/profile-conditions-util.js'; +import {createSchema, normalizeContext} from '../ext/js/background/profile-conditions-util.js'; /** */ function testNormalizeContext() { @@ -50,8 +50,7 @@ function testNormalizeContext() { ]; test.each(data)('normalize-context-test-%#', ({context, expected}) => { - const profileConditionsUtil = new ProfileConditionsUtil(); - const actual = profileConditionsUtil.normalizeContext(context); + const actual = normalizeContext(context); expect(actual).toStrictEqual(expected); }); }); @@ -1101,14 +1100,13 @@ function testSchemas() { /* eslint-enable no-multi-spaces */ test.each(data)('schemas-test-%#', ({conditionGroups, expectedSchema, inputs}) => { - const profileConditionsUtil = new ProfileConditionsUtil(); - const schema = profileConditionsUtil.createSchema(conditionGroups); + const schema = createSchema(conditionGroups); if (typeof expectedSchema !== 'undefined') { expect(schema.schema).toStrictEqual(expectedSchema); } if (Array.isArray(inputs)) { for (const {expected, context} of inputs) { - const normalizedContext = profileConditionsUtil.normalizeContext(context); + const normalizedContext = normalizeContext(context); const actual = schema.isValid(normalizedContext); expect(actual).toStrictEqual(expected); } diff --git a/test/utilities/anki.js b/test/utilities/anki.js index e30d578f..69f4ce8b 100644 --- a/test/utilities/anki.js +++ b/test/utilities/anki.js @@ -16,16 +16,16 @@ */ import {AnkiNoteBuilder} from '../../ext/js/data/anki-note-builder.js'; +import {createAnkiNoteData} from '../../ext/js/data/sandbox/anki-note-data-creator.js'; import {AnkiTemplateRenderer} from '../../ext/js/templates/sandbox/anki-template-renderer.js'; /** - * @param {import('../../ext/js/data/sandbox/anki-note-data-creator.js').AnkiNoteDataCreator} ankiNoteDataCreator * @param {import('dictionary').DictionaryEntry} dictionaryEntry * @param {import('settings').ResultOutputMode} mode * @returns {import('anki-templates').NoteData} * @throws {Error} */ -export function createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, mode) { +export function createTestAnkiNoteData(dictionaryEntry, mode) { const marker = '{marker}'; /** @type {import('anki-templates-internal').CreateDetails} */ const data = { @@ -43,7 +43,7 @@ export function createTestAnkiNoteData(ankiNoteDataCreator, dictionaryEntry, mod }, media: {} }; - return ankiNoteDataCreator.create(marker, data); + return createAnkiNoteData(marker, data); } /** |