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 /ext/js | |
| 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
Diffstat (limited to 'ext/js')
33 files changed, 2116 insertions, 2191 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 '';          } |