diff options
Diffstat (limited to 'ext/js/data')
| -rw-r--r-- | ext/js/data/anki-note-builder.js | 144 | ||||
| -rw-r--r-- | ext/js/data/anki-util.js | 2 | ||||
| -rw-r--r-- | ext/js/data/database.js | 206 | ||||
| -rw-r--r-- | ext/js/data/json-schema.js | 927 | ||||
| -rw-r--r-- | ext/js/data/options-util.js | 192 | ||||
| -rw-r--r-- | ext/js/data/permissions-util.js | 22 | ||||
| -rw-r--r-- | ext/js/data/sandbox/anki-note-data-creator.js | 245 | 
7 files changed, 1374 insertions, 364 deletions
| diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js index 398036c0..4920db39 100644 --- a/ext/js/data/anki-note-builder.js +++ b/ext/js/data/anki-note-builder.js @@ -16,20 +16,33 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ -import {deferPromise, deserializeError} from '../core.js'; +import {deferPromise} from '../core.js'; +import {ExtensionError} from '../core/extension-error.js';  import {TemplateRendererProxy} from '../templates/template-renderer-proxy.js';  import {yomitan} from '../yomitan.js';  import {AnkiUtil} from './anki-util.js';  export class AnkiNoteBuilder { +    /** +     * @param {{japaneseUtil: import('../language/sandbox/japanese-util.js').JapaneseUtil}} details +     */      constructor({japaneseUtil}) { +        /** @type {import('../language/sandbox/japanese-util.js').JapaneseUtil} */          this._japaneseUtil = japaneseUtil; +        /** @type {RegExp} */          this._markerPattern = AnkiUtil.cloneFieldMarkerPattern(true); +        /** @type {TemplateRendererProxy} */          this._templateRenderer = new TemplateRendererProxy(); +        /** @type {import('anki-note-builder').BatchedRequestGroup[]} */          this._batchedRequests = []; +        /** @type {boolean} */          this._batchedRequestsQueued = false;      } +    /** +     * @param {import('anki-note-builder').CreateNoteDetails} details +     * @returns {Promise<import('anki-note-builder').CreateNoteResult>} +     */      async createNote({          dictionaryEntry,          mode, @@ -56,16 +69,15 @@ export class AnkiNoteBuilder {              duplicateScopeCheckChildren = true;          } +        /** @type {Error[]} */          const allErrors = [];          let media;          if (requirements.length > 0 && mediaOptions !== null) {              let errors;              ({media, errors} = await this._injectMedia(dictionaryEntry, requirements, mediaOptions));              for (const error of errors) { -                allErrors.push(deserializeError(error)); +                allErrors.push(ExtensionError.deserialize(error));              } -        } else { -            media = {};          }          const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, media); @@ -77,6 +89,7 @@ export class AnkiNoteBuilder {          const formattedFieldValues = await Promise.all(formattedFieldValuePromises);          const uniqueRequirements = new Map(); +        /** @type {import('anki').NoteFields} */          const noteFields = {};          for (let i = 0, ii = fields.length; i < ii; ++i) {              const fieldName = fields[i][0]; @@ -90,6 +103,7 @@ export class AnkiNoteBuilder {              }          } +        /** @type {import('anki').Note} */          const note = {              fields: noteFields,              tags, @@ -108,6 +122,10 @@ export class AnkiNoteBuilder {          return {note, errors: allErrors, requirements: [...uniqueRequirements.values()]};      } +    /** +     * @param {import('anki-note-builder').GetRenderingDataDetails} details +     * @returns {Promise<import('anki-templates').NoteData>} +     */      async getRenderingData({          dictionaryEntry,          mode, @@ -115,12 +133,16 @@ export class AnkiNoteBuilder {          resultOutputMode='split',          glossaryLayoutMode='default',          compactTags=false, -        marker=null +        marker      }) { -        const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, {}); +        const commonData = this._createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, void 0);          return await this._templateRenderer.getModifiedData({marker, commonData}, 'ankiNote');      } +    /** +     * @param {import('dictionary').DictionaryEntry} dictionaryEntry +     * @returns {import('api').InjectAnkiNoteMediaDefinitionDetails} +     */      getDictionaryEntryDetailsForNote(dictionaryEntry) {          const {type} = dictionaryEntry;          if (type === 'kanji') { @@ -150,6 +172,16 @@ export class AnkiNoteBuilder {      // Private +    /** +     * @param {import('dictionary').DictionaryEntry} dictionaryEntry +     * @param {import('anki-templates-internal').CreateMode} mode +     * @param {import('anki-templates-internal').Context} context +     * @param {import('settings').ResultOutputMode} resultOutputMode +     * @param {import('settings').GlossaryLayoutMode} glossaryLayoutMode +     * @param {boolean} compactTags +     * @param {import('anki-templates').Media|undefined} media +     * @returns {import('anki-note-builder').CommonData} +     */      _createData(dictionaryEntry, mode, context, resultOutputMode, glossaryLayoutMode, compactTags, media) {          return {              dictionaryEntry, @@ -162,16 +194,25 @@ export class AnkiNoteBuilder {          };      } +    /** +     * @param {string} field +     * @param {import('anki-note-builder').CommonData} commonData +     * @param {string} template +     * @returns {Promise<{value: string, errors: ExtensionError[], requirements: import('anki-note-builder').Requirement[]}>} +     */      async _formatField(field, commonData, template) { +        /** @type {ExtensionError[]} */          const errors = []; +        /** @type {import('anki-note-builder').Requirement[]} */          const requirements = []; -        const value = await this._stringReplaceAsync(field, this._markerPattern, async (g0, marker) => { +        const value = await this._stringReplaceAsync(field, this._markerPattern, async (match) => { +            const marker = match[1];              try {                  const {result, requirements: fieldRequirements} = await this._renderTemplateBatched(template, commonData, marker);                  requirements.push(...fieldRequirements);                  return result;              } catch (e) { -                const error = new Error(`Template render error for {${marker}}`); +                const error = new ExtensionError(`Template render error for {${marker}}`);                  error.data = {error: e};                  errors.push(error);                  return `{${marker}-render-error}`; @@ -180,12 +221,19 @@ export class AnkiNoteBuilder {          return {value, errors, requirements};      } +    /** +     * @param {string} str +     * @param {RegExp} regex +     * @param {(match: RegExpExecArray, index: number, str: string) => (string|Promise<string>)} replacer +     * @returns {Promise<string>} +     */      async _stringReplaceAsync(str, regex, replacer) {          let match;          let index = 0; +        /** @type {(Promise<string>|string)[]} */          const parts = [];          while ((match = regex.exec(str)) !== null) { -            parts.push(str.substring(index, match.index), replacer(...match, match.index, str)); +            parts.push(str.substring(index, match.index), replacer(match, match.index, str));              index = regex.lastIndex;          }          if (parts.length === 0) { @@ -195,6 +243,10 @@ export class AnkiNoteBuilder {          return (await Promise.all(parts)).join('');      } +    /** +     * @param {string} template +     * @returns {import('anki-note-builder').BatchedRequestGroup} +     */      _getBatchedTemplateGroup(template) {          for (const item of this._batchedRequests) {              if (item.template === template) { @@ -207,7 +259,14 @@ export class AnkiNoteBuilder {          return result;      } +    /** +     * @param {string} template +     * @param {import('anki-note-builder').CommonData} commonData +     * @param {string} marker +     * @returns {Promise<import('template-renderer').RenderResult>} +     */      _renderTemplateBatched(template, commonData, marker) { +        /** @type {import('core').DeferredPromiseDetails<import('template-renderer').RenderResult>} */          const {promise, resolve, reject} = deferPromise();          const {commonDataRequestsMap} = this._getBatchedTemplateGroup(template);          let requests = commonDataRequestsMap.get(commonData); @@ -220,6 +279,9 @@ export class AnkiNoteBuilder {          return promise;      } +    /** +     * @returns {void} +     */      _runBatchedRequestsDelayed() {          if (this._batchedRequestsQueued) { return; }          this._batchedRequestsQueued = true; @@ -229,20 +291,30 @@ export class AnkiNoteBuilder {          });      } +    /** +     * @returns {void} +     */      _runBatchedRequests() {          if (this._batchedRequests.length === 0) { return; }          const allRequests = []; +        /** @type {import('template-renderer').RenderMultiItem[]} */          const items = [];          for (const {template, commonDataRequestsMap} of this._batchedRequests) { +            /** @type {import('template-renderer').RenderMultiTemplateItem[]} */              const templateItems = [];              for (const [commonData, requests] of commonDataRequestsMap.entries()) { +                /** @type {import('template-renderer').PartialOrCompositeRenderData[]} */                  const datas = [];                  for (const {marker} of requests) { -                    datas.push(marker); +                    datas.push({marker});                  }                  allRequests.push(...requests); -                templateItems.push({type: 'ankiNote', commonData, datas}); +                templateItems.push({ +                    type: /** @type {import('anki-templates').RenderMode} */ ('ankiNote'), +                    commonData, +                    datas +                });              }              items.push({template, templateItems});          } @@ -252,6 +324,10 @@ export class AnkiNoteBuilder {          this._resolveBatchedRequests(items, allRequests);      } +    /** +     * @param {import('template-renderer').RenderMultiItem[]} items +     * @param {import('anki-note-builder').BatchedRequestData[]} requests +     */      async _resolveBatchedRequests(items, requests) {          let responses;          try { @@ -269,7 +345,7 @@ export class AnkiNoteBuilder {                  const response = responses[i];                  const {error} = response;                  if (typeof error !== 'undefined') { -                    throw deserializeError(error); +                    throw ExtensionError.deserialize(error);                  } else {                      request.resolve(response.result);                  } @@ -279,6 +355,12 @@ export class AnkiNoteBuilder {          }      } +    /** +     * @param {import('dictionary').DictionaryEntry} dictionaryEntry +     * @param {import('anki-note-builder').Requirement[]} requirements +     * @param {import('anki-note-builder').MediaOptions} mediaOptions +     * @returns {Promise<{media: import('anki-templates').Media, errors: import('core').SerializedError[]}>} +     */      async _injectMedia(dictionaryEntry, requirements, mediaOptions) {          const timestamp = Date.now(); @@ -288,7 +370,9 @@ export class AnkiNoteBuilder {          let injectClipboardImage = false;          let injectClipboardText = false;          let injectSelectionText = false; +        /** @type {import('anki-note-builder').TextFuriganaDetails[]} */          const textFuriganaDetails = []; +        /** @type {import('api').InjectAnkiNoteMediaDictionaryMediaDetails[]} */          const dictionaryMediaDetails = [];          for (const requirement of requirements) {              const {type} = requirement; @@ -315,8 +399,11 @@ export class AnkiNoteBuilder {          // Generate request data          const dictionaryEntryDetails = this.getDictionaryEntryDetailsForNote(dictionaryEntry); +        /** @type {?import('api').InjectAnkiNoteMediaAudioDetails} */          let audioDetails = null; +        /** @type {?import('api').InjectAnkiNoteMediaScreenshotDetails} */          let screenshotDetails = null; +        /** @type {import('api').InjectAnkiNoteMediaClipboardDetails} */          const clipboardDetails = {image: injectClipboardImage, text: injectClipboardText};          if (injectAudio && dictionaryEntryDetails.type !== 'kanji') {              const audioOptions = mediaOptions.audio; @@ -357,6 +444,7 @@ export class AnkiNoteBuilder {          const textFurigana = textFuriganaPromise !== null ? await textFuriganaPromise : [];          // Format results +        /** @type {import('anki-templates').DictionaryMedia} */          const dictionaryMedia = {};          for (const {dictionary, path, fileName} of dictionaryMediaArray) {              if (fileName === null) { continue; } @@ -368,21 +456,31 @@ export class AnkiNoteBuilder {              dictionaryMedia2[path] = {value: fileName};          }          const media = { -            audio: (typeof audioFileName === 'string' ? {value: audioFileName} : null), -            screenshot: (typeof screenshotFileName === 'string' ? {value: screenshotFileName} : null), -            clipboardImage: (typeof clipboardImageFileName === 'string' ? {value: clipboardImageFileName} : null), -            clipboardText: (typeof clipboardText === 'string' ? {value: clipboardText} : null), -            selectionText: (typeof selectionText === 'string' ? {value: selectionText} : null), +            audio: (typeof audioFileName === 'string' ? {value: audioFileName} : void 0), +            screenshot: (typeof screenshotFileName === 'string' ? {value: screenshotFileName} : void 0), +            clipboardImage: (typeof clipboardImageFileName === 'string' ? {value: clipboardImageFileName} : void 0), +            clipboardText: (typeof clipboardText === 'string' ? {value: clipboardText} : void 0), +            selectionText: (typeof selectionText === 'string' ? {value: selectionText} : void 0),              textFurigana,              dictionaryMedia          };          return {media, errors};      } +    /** +     * @returns {string} +     */      _getSelectionText() { -        return document.getSelection().toString(); +        const selection = document.getSelection(); +        return selection !== null ? selection.toString() : '';      } +    /** +     * @param {import('anki-note-builder').TextFuriganaDetails[]} entries +     * @param {import('settings').OptionsContext} optionsContext +     * @param {number} scanLength +     * @returns {Promise<import('anki-templates').TextFuriganaSegment[]>} +     */      async _getTextFurigana(entries, optionsContext, scanLength) {          const results = [];          for (const {text, readingMode} of entries) { @@ -401,6 +499,11 @@ export class AnkiNoteBuilder {          return results;      } +    /** +     * @param {import('api').ParseTextLine[]} data +     * @param {?import('anki-templates').TextFuriganaReadingMode} readingMode +     * @returns {string} +     */      _createFuriganaHtml(data, readingMode) {          let result = '';          for (const term of data) { @@ -418,6 +521,11 @@ export class AnkiNoteBuilder {          return result;      } +    /** +     * @param {string} reading +     * @param {?import('anki-templates').TextFuriganaReadingMode} readingMode +     * @returns {string} +     */      _convertReading(reading, readingMode) {          switch (readingMode) {              case 'hiragana': diff --git a/ext/js/data/anki-util.js b/ext/js/data/anki-util.js index c08b562e..1d5272a6 100644 --- a/ext/js/data/anki-util.js +++ b/ext/js/data/anki-util.js @@ -71,7 +71,7 @@ export class AnkiUtil {      /**       * Checks whether or not a note object is valid. -     * @param {*} note A note object to check. +     * @param {import('anki').Note} note A note object to check.       * @returns {boolean} `true` if the note is valid, `false` otherwise.       */      static isNoteDataValid(note) { diff --git a/ext/js/data/database.js b/ext/js/data/database.js index 8e818d8b..026945ca 100644 --- a/ext/js/data/database.js +++ b/ext/js/data/database.js @@ -16,12 +16,22 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ +/** + * @template {string} TObjectStoreName + */  export class Database {      constructor() { +        /** @type {?IDBDatabase} */          this._db = null; +        /** @type {boolean} */          this._isOpening = false;      } +    /** +     * @param {string} databaseName +     * @param {number} version +     * @param {import('database').StructureDefinition<TObjectStoreName>[]} structure +     */      async open(databaseName, version, structure) {          if (this._db !== null) {              throw new Error('Database already open'); @@ -40,6 +50,9 @@ export class Database {          }      } +    /** +     * @throws {Error} +     */      close() {          if (this._db === null) {              throw new Error('Database is not open'); @@ -49,14 +62,26 @@ export class Database {          this._db = null;      } +    /** +     * @returns {boolean} +     */      isOpening() {          return this._isOpening;      } +    /** +     * @returns {boolean} +     */      isOpen() {          return this._db !== null;      } +    /** +     * @param {string[]} storeNames +     * @param {IDBTransactionMode} mode +     * @returns {IDBTransaction} +     * @throws {Error} +     */      transaction(storeNames, mode) {          if (this._db === null) {              throw new Error(this._isOpening ? 'Database not ready' : 'Database not open'); @@ -64,6 +89,13 @@ export class Database {          return this._db.transaction(storeNames, mode);      } +    /** +     * @param {TObjectStoreName} objectStoreName +     * @param {unknown[]} items +     * @param {number} start +     * @param {number} count +     * @returns {Promise<void>} +     */      bulkAdd(objectStoreName, items, start, count) {          return new Promise((resolve, reject) => {              if (start + count > items.length) { @@ -84,6 +116,15 @@ export class Database {          });      } +    /** +     * @template [TData=unknown] +     * @template [TResult=unknown] +     * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex +     * @param {?IDBValidKey|IDBKeyRange} query +     * @param {(results: TResult[], data: TData) => void} onSuccess +     * @param {(reason: unknown, data: TData) => void} onError +     * @param {TData} data +     */      getAll(objectStoreOrIndex, query, onSuccess, onError, data) {          if (typeof objectStoreOrIndex.getAll === 'function') {              this._getAllFast(objectStoreOrIndex, query, onSuccess, onError, data); @@ -92,6 +133,12 @@ export class Database {          }      } +    /** +     * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex +     * @param {IDBValidKey|IDBKeyRange} query +     * @param {(value: IDBValidKey[]) => void} onSuccess +     * @param {(reason?: unknown) => void} onError +     */      getAllKeys(objectStoreOrIndex, query, onSuccess, onError) {          if (typeof objectStoreOrIndex.getAllKeys === 'function') {              this._getAllKeysFast(objectStoreOrIndex, query, onSuccess, onError); @@ -100,6 +147,18 @@ export class Database {          }      } +    /** +     * @template TPredicateArg +     * @template [TResult=unknown] +     * @template [TResultDefault=unknown] +     * @param {TObjectStoreName} objectStoreName +     * @param {?string} indexName +     * @param {?IDBValidKey|IDBKeyRange} query +     * @param {?((value: TResult|TResultDefault, predicateArg: TPredicateArg) => boolean)} predicate +     * @param {TPredicateArg} predicateArg +     * @param {TResultDefault} defaultValue +     * @returns {Promise<TResult|TResultDefault>} +     */      find(objectStoreName, indexName, query, predicate, predicateArg, defaultValue) {          return new Promise((resolve, reject) => {              const transaction = this.transaction([objectStoreName], 'readonly'); @@ -109,12 +168,26 @@ export class Database {          });      } +    /** +     * @template TData +     * @template TPredicateArg +     * @template [TResult=unknown] +     * @template [TResultDefault=unknown] +     * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex +     * @param {?IDBValidKey|IDBKeyRange} query +     * @param {(value: TResult|TResultDefault, data: TData) => void} resolve +     * @param {(reason: unknown, data: TData) => void} reject +     * @param {TData} data +     * @param {?((value: TResult, predicateArg: TPredicateArg) => boolean)} predicate +     * @param {TPredicateArg} predicateArg +     * @param {TResultDefault} defaultValue +     */      findFirst(objectStoreOrIndex, query, resolve, reject, data, predicate, predicateArg, defaultValue) {          const noPredicate = (typeof predicate !== 'function');          const request = objectStoreOrIndex.openCursor(query, 'next'); -        request.onerror = (e) => reject(e.target.error, data); +        request.onerror = (e) => reject(/** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).error, data);          request.onsuccess = (e) => { -            const cursor = e.target.result; +            const cursor = /** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).result;              if (cursor) {                  const {value} = cursor;                  if (noPredicate || predicate(value, predicateArg)) { @@ -128,19 +201,33 @@ export class Database {          };      } +    /** +     * @param {import('database').CountTarget[]} targets +     * @param {(results: number[]) => void} resolve +     * @param {(reason?: unknown) => void} reject +     */      bulkCount(targets, resolve, reject) {          const targetCount = targets.length;          if (targetCount <= 0) { -            resolve(); +            resolve([]);              return;          }          let completedCount = 0; +        /** @type {number[]} */          const results = new Array(targetCount).fill(null); -        const onError = (e) => reject(e.target.error); +        /** +         * @param {Event} e +         * @returns {void} +         */ +        const onError = (e) => reject(/** @type {IDBRequest<number>} */ (e.target).error); +        /** +         * @param {Event} e +         * @param {number} index +         */          const onSuccess = (e, index) => { -            const count = e.target.result; +            const count = /** @type {IDBRequest<number>} */ (e.target).result;              results[index] = count;              if (++completedCount >= targetCount) {                  resolve(results); @@ -156,6 +243,11 @@ export class Database {          }      } +    /** +     * @param {TObjectStoreName} objectStoreName +     * @param {IDBValidKey|IDBKeyRange} key +     * @returns {Promise<void>} +     */      delete(objectStoreName, key) {          return new Promise((resolve, reject) => {              const transaction = this._readWriteTransaction([objectStoreName], resolve, reject); @@ -165,12 +257,23 @@ export class Database {          });      } +    /** +     * @param {TObjectStoreName} objectStoreName +     * @param {?string} indexName +     * @param {IDBKeyRange} query +     * @param {?(keys: IDBValidKey[]) => IDBValidKey[]} filterKeys +     * @param {?(completedCount: number, totalCount: number) => void} onProgress +     * @returns {Promise<void>} +     */      bulkDelete(objectStoreName, indexName, query, filterKeys=null, onProgress=null) {          return new Promise((resolve, reject) => {              const transaction = this._readWriteTransaction([objectStoreName], resolve, reject);              const objectStore = transaction.objectStore(objectStoreName);              const objectStoreOrIndex = indexName !== null ? objectStore.index(indexName) : objectStore; +            /** +             * @param {IDBValidKey[]} keys +             */              const onGetKeys = (keys) => {                  try {                      if (typeof filterKeys === 'function') { @@ -187,10 +290,14 @@ export class Database {          });      } +    /** +     * @param {string} databaseName +     * @returns {Promise<void>} +     */      static deleteDatabase(databaseName) {          return new Promise((resolve, reject) => {              const request = indexedDB.deleteDatabase(databaseName); -            request.onerror = (e) => reject(e.target.error); +            request.onerror = (e) => reject(/** @type {IDBRequest} */ (e.target).error);              request.onsuccess = () => resolve();              request.onblocked = () => reject(new Error('Database deletion blocked'));          }); @@ -198,24 +305,37 @@ export class Database {      // Private +    /** +     * @param {string} name +     * @param {number} version +     * @param {import('database').UpdateFunction} onUpgradeNeeded +     * @returns {Promise<IDBDatabase>} +     */      _open(name, version, onUpgradeNeeded) {          return new Promise((resolve, reject) => {              const request = indexedDB.open(name, version);              request.onupgradeneeded = (event) => {                  try { -                    request.transaction.onerror = (e) => reject(e.target.error); -                    onUpgradeNeeded(request.result, request.transaction, event.oldVersion, event.newVersion); +                    const transaction = /** @type {IDBTransaction} */ (request.transaction); +                    transaction.onerror = (e) => reject(/** @type {IDBRequest} */ (e.target).error); +                    onUpgradeNeeded(request.result, transaction, event.oldVersion, event.newVersion);                  } catch (e) {                      reject(e);                  }              }; -            request.onerror = (e) => reject(e.target.error); +            request.onerror = (e) => reject(/** @type {IDBRequest} */ (e.target).error);              request.onsuccess = () => resolve(request.result);          });      } +    /** +     * @param {IDBDatabase} db +     * @param {IDBTransaction} transaction +     * @param {number} oldVersion +     * @param {import('database').StructureDefinition<TObjectStoreName>[]} upgrades +     */      _upgrade(db, transaction, oldVersion, upgrades) {          for (const {version, stores} of upgrades) {              if (oldVersion >= version) { continue; } @@ -238,6 +358,11 @@ export class Database {          }      } +    /** +     * @param {DOMStringList} list +     * @param {string} value +     * @returns {boolean} +     */      _listContains(list, value) {          for (let i = 0, ii = list.length; i < ii; ++i) {              if (list[i] === value) { return true; } @@ -245,18 +370,37 @@ export class Database {          return false;      } +    /** +     * @template [TData=unknown] +     * @template [TResult=unknown] +     * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex +     * @param {?IDBValidKey|IDBKeyRange} query +     * @param {(results: TResult[], data: TData) => void} onSuccess +     * @param {(reason: unknown, data: TData) => void} onReject +     * @param {TData} data +     */      _getAllFast(objectStoreOrIndex, query, onSuccess, onReject, data) {          const request = objectStoreOrIndex.getAll(query); -        request.onerror = (e) => onReject(e.target.error, data); -        request.onsuccess = (e) => onSuccess(e.target.result, data); +        request.onerror = (e) => onReject(/** @type {IDBRequest<import('core').SafeAny[]>} */ (e.target).error, data); +        request.onsuccess = (e) => onSuccess(/** @type {IDBRequest<import('core').SafeAny[]>} */ (e.target).result, data);      } +    /** +     * @template [TData=unknown] +     * @template [TResult=unknown] +     * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex +     * @param {?IDBValidKey|IDBKeyRange} query +     * @param {(results: TResult[], data: TData) => void} onSuccess +     * @param {(reason: unknown, data: TData) => void} onReject +     * @param {TData} data +     */      _getAllUsingCursor(objectStoreOrIndex, query, onSuccess, onReject, data) { +        /** @type {TResult[]} */          const results = [];          const request = objectStoreOrIndex.openCursor(query, 'next'); -        request.onerror = (e) => onReject(e.target.error, data); +        request.onerror = (e) => onReject(/** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).error, data);          request.onsuccess = (e) => { -            const cursor = e.target.result; +            const cursor = /** @type {IDBRequest<?IDBCursorWithValue>} */ (e.target).result;              if (cursor) {                  results.push(cursor.value);                  cursor.continue(); @@ -266,18 +410,31 @@ export class Database {          };      } +    /** +     * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex +     * @param {IDBValidKey|IDBKeyRange} query +     * @param {(value: IDBValidKey[]) => void} onSuccess +     * @param {(reason?: unknown) => void} onError +     */      _getAllKeysFast(objectStoreOrIndex, query, onSuccess, onError) {          const request = objectStoreOrIndex.getAllKeys(query); -        request.onerror = (e) => onError(e.target.error); -        request.onsuccess = (e) => onSuccess(e.target.result); +        request.onerror = (e) => onError(/** @type {IDBRequest<IDBValidKey[]>} */ (e.target).error); +        request.onsuccess = (e) => onSuccess(/** @type {IDBRequest<IDBValidKey[]>} */ (e.target).result);      } +    /** +     * @param {IDBObjectStore|IDBIndex} objectStoreOrIndex +     * @param {IDBValidKey|IDBKeyRange} query +     * @param {(value: IDBValidKey[]) => void} onSuccess +     * @param {(reason?: unknown) => void} onError +     */      _getAllKeysUsingCursor(objectStoreOrIndex, query, onSuccess, onError) { +        /** @type {IDBValidKey[]} */          const results = [];          const request = objectStoreOrIndex.openKeyCursor(query, 'next'); -        request.onerror = (e) => onError(e.target.error); +        request.onerror = (e) => onError(/** @type {IDBRequest<?IDBCursor>} */ (e.target).error);          request.onsuccess = (e) => { -            const cursor = e.target.result; +            const cursor = /** @type {IDBRequest<?IDBCursor>} */ (e.target).result;              if (cursor) {                  results.push(cursor.primaryKey);                  cursor.continue(); @@ -287,6 +444,11 @@ export class Database {          };      } +    /** +     * @param {IDBObjectStore} objectStore +     * @param {IDBValidKey[]} keys +     * @param {?(completedCount: number, totalCount: number) => void} onProgress +     */      _bulkDeleteInternal(objectStore, keys, onProgress) {          const count = keys.length;          if (count === 0) { return; } @@ -295,7 +457,7 @@ export class Database {          const onSuccess = () => {              ++completedCount;              try { -                onProgress(completedCount, count); +                /** @type {(completedCount: number, totalCount: number) => void}} */ (onProgress)(completedCount, count);              } catch (e) {                  // NOP              } @@ -310,9 +472,15 @@ export class Database {          }      } +    /** +     * @param {string[]} storeNames +     * @param {() => void} resolve +     * @param {(reason?: unknown) => void} reject +     * @returns {IDBTransaction} +     */      _readWriteTransaction(storeNames, resolve, reject) {          const transaction = this.transaction(storeNames, 'readwrite'); -        transaction.onerror = (e) => reject(e.target.error); +        transaction.onerror = (e) => reject(/** @type {IDBTransaction} */ (e.target).error);          transaction.onabort = () => reject(new Error('Transaction aborted'));          transaction.oncomplete = () => resolve();          return transaction; diff --git a/ext/js/data/json-schema.js b/ext/js/data/json-schema.js index 93c8cd59..d63cfd1a 100644 --- a/ext/js/data/json-schema.js +++ b/ext/js/data/json-schema.js @@ -19,31 +19,70 @@  import {clone} from '../core.js';  import {CacheMap} from '../general/cache-map.js'; +export class JsonSchemaError extends Error { +    /** +     * @param {string} message +     * @param {import('json-schema').ValueStackItem[]} valueStack +     * @param {import('json-schema').SchemaStackItem[]} schemaStack +     */ +    constructor(message, valueStack, schemaStack) { +        super(message); +        /** @type {import('json-schema').ValueStackItem[]} */ +        this._valueStack = valueStack; +        /** @type {import('json-schema').SchemaStackItem[]} */ +        this._schemaStack = schemaStack; +    } + +    /** @type {unknown|undefined} */ +    get value() { return this._valueStack.length > 0 ? this._valueStack[this._valueStack.length - 1].value : void 0; } + +    /** @type {import('json-schema').Schema|import('json-schema').Schema[]|undefined} */ +    get schema() { return this._schemaStack.length > 0 ? this._schemaStack[this._schemaStack.length - 1].schema : void 0; } + +    /** @type {import('json-schema').ValueStackItem[]} */ +    get valueStack() { return this._valueStack; } + +    /** @type {import('json-schema').SchemaStackItem[]} */ +    get schemaStack() { return this._schemaStack; } +} +  export class JsonSchema { +    /** +     * @param {import('json-schema').Schema} schema +     * @param {import('json-schema').Schema} [rootSchema] +     */      constructor(schema, rootSchema) { -        this._schema = null; +        /** @type {import('json-schema').Schema} */          this._startSchema = schema; +        /** @type {import('json-schema').Schema} */          this._rootSchema = typeof rootSchema !== 'undefined' ? rootSchema : schema; +        /** @type {?CacheMap<string, RegExp>} */          this._regexCache = null; +        /** @type {?Map<string, {schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}>} */          this._refCache = null; +        /** @type {import('json-schema').ValueStackItem[]} */          this._valueStack = []; +        /** @type {import('json-schema').SchemaStackItem[]} */          this._schemaStack = []; +        /** @type {?(jsonSchema: JsonSchema) => void} */          this._progress = null; +        /** @type {number} */          this._progressCounter = 0; +        /** @type {number} */          this._progressInterval = 1; - -        this._schemaPush(null, null); -        this._valuePush(null, null);      } +    /** @type {import('json-schema').Schema} */      get schema() {          return this._startSchema;      } +    /** @type {import('json-schema').Schema} */      get rootSchema() {          return this._rootSchema;      } +    /** @type {?(jsonSchema: JsonSchema) => void} */      get progress() {          return this._progress;      } @@ -52,6 +91,7 @@ export class JsonSchema {          this._progress = value;      } +    /** @type {number} */      get progressInterval() {          return this._progressInterval;      } @@ -60,6 +100,10 @@ export class JsonSchema {          this._progressInterval = value;      } +    /** +     * @param {import('json-schema').Value} value +     * @returns {import('json-schema').Value} +     */      createProxy(value) {          return (              typeof value === 'object' && value !== null ? @@ -68,6 +112,10 @@ export class JsonSchema {          );      } +    /** +     * @param {unknown} value +     * @returns {boolean} +     */      isValid(value) {          try {              this.validate(value); @@ -77,123 +125,203 @@ export class JsonSchema {          }      } +    /** +     * @param {unknown} value +     */      validate(value) { -        this._schemaPush(this._startSchema, null); +        const schema = this._startSchema; +        this._schemaPush(schema, null);          this._valuePush(value, null);          try { -            this._validate(value); +            this._validate(schema, value);          } finally {              this._valuePop();              this._schemaPop();          }      } +    /** +     * @param {unknown} [value] +     * @returns {import('json-schema').Value} +     */      getValidValueOrDefault(value) { -        return this._getValidValueOrDefault(null, value, {schema: this._startSchema, path: null}); +        const schema = this._startSchema; +        return this._getValidValueOrDefault(schema, null, value, [{schema, path: null}]);      } +    /** +     * @param {string} property +     * @returns {?JsonSchema} +     */      getObjectPropertySchema(property) { -        const startSchemaInfo = this._getResolveSchemaInfo({schema: this._startSchema, path: null}); -        this._schemaPush(startSchemaInfo.schema, startSchemaInfo.path); +        const schema = this._startSchema; +        const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, [{schema, path: null}]); +        this._schemaPushMultiple(stack);          try { -            const schemaInfo = this._getObjectPropertySchemaInfo(property); -            return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null; +            const {schema: propertySchema} = this._getObjectPropertySchemaInfo(schema2, property); +            return propertySchema !== false ? new JsonSchema(propertySchema, this._rootSchema) : null;          } finally { -            this._schemaPop(); +            this._schemaPopMultiple(stack.length);          }      } +    /** +     * @param {number} index +     * @returns {?JsonSchema} +     */      getArrayItemSchema(index) { -        const startSchemaInfo = this._getResolveSchemaInfo({schema: this._startSchema, path: null}); -        this._schemaPush(startSchemaInfo.schema, startSchemaInfo.path); +        const schema = this._startSchema; +        const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, [{schema, path: null}]); +        this._schemaPushMultiple(stack);          try { -            const schemaInfo = this._getArrayItemSchemaInfo(index); -            return schemaInfo !== null ? new JsonSchema(schemaInfo.schema, this._rootSchema) : null; +            const {schema: itemSchema} = this._getArrayItemSchemaInfo(schema2, index); +            return itemSchema !== false ? new JsonSchema(itemSchema, this._rootSchema) : null;          } finally { -            this._schemaPop(); +            this._schemaPopMultiple(stack.length);          }      } +    /** +     * @param {string} property +     * @returns {boolean} +     */      isObjectPropertyRequired(property) { -        const {required} = this._startSchema; +        const schema = this._startSchema; +        if (typeof schema === 'boolean') { return false; } +        const {required} = schema;          return Array.isArray(required) && required.includes(property);      }      // Internal state functions for error construction and progress callback +    /** +     * @returns {import('json-schema').ValueStackItem[]} +     */      getValueStack() { -        const valueStack = []; -        for (let i = 1, ii = this._valueStack.length; i < ii; ++i) { -            const {value, path} = this._valueStack[i]; -            valueStack.push({value, path}); +        const result = []; +        for (const {value, path} of this._valueStack) { +            result.push({value, path});          } -        return valueStack; +        return result;      } +    /** +     * @returns {import('json-schema').SchemaStackItem[]} +     */      getSchemaStack() { -        const schemaStack = []; -        for (let i = 1, ii = this._schemaStack.length; i < ii; ++i) { -            const {schema, path} = this._schemaStack[i]; -            schemaStack.push({schema, path}); +        const result = []; +        for (const {schema, path} of this._schemaStack) { +            result.push({schema, path});          } -        return schemaStack; +        return result;      } +    /** +     * @returns {number} +     */      getValueStackLength() {          return this._valueStack.length - 1;      } +    /** +     * @param {number} index +     * @returns {import('json-schema').ValueStackItem} +     */      getValueStackItem(index) {          const {value, path} = this._valueStack[index + 1];          return {value, path};      } +    /** +     * @returns {number} +     */      getSchemaStackLength() {          return this._schemaStack.length - 1;      } +    /** +     * @param {number} index +     * @returns {import('json-schema').SchemaStackItem} +     */      getSchemaStackItem(index) {          const {schema, path} = this._schemaStack[index + 1];          return {schema, path};      } +    /** +     * @template T +     * @param {T} value +     * @returns {T} +     */ +    static clone(value) { +        return clone(value); +    } +      // Stack +    /** +     * @param {unknown} value +     * @param {string|number|null} path +     */      _valuePush(value, path) {          this._valueStack.push({value, path});      } +    /** +     * @returns {void} +     */      _valuePop() {          this._valueStack.pop();      } +    /** +     * @param {import('json-schema').Schema|import('json-schema').Schema[]} schema +     * @param {string|number|null} path +     */      _schemaPush(schema, path) {          this._schemaStack.push({schema, path}); -        this._schema = schema;      } +    /** +     * @param {import('json-schema').SchemaStackItem[]} items +     */ +    _schemaPushMultiple(items) { +        this._schemaStack.push(...items); +    } + +    /** +     * @returns {void} +     */      _schemaPop() {          this._schemaStack.pop(); -        this._schema = this._schemaStack[this._schemaStack.length - 1].schema; +    } + +    /** +     * @param {number} count +     */ +    _schemaPopMultiple(count) { +        for (let i = 0; i < count; ++i) { +            this._schemaStack.pop(); +        }      }      // Private +    /** +     * @param {string} message +     * @returns {JsonSchemaError} +     */      _createError(message) {          const valueStack = this.getValueStack();          const schemaStack = this.getSchemaStack(); -        const error = new Error(message); -        error.value = valueStack[valueStack.length - 1].value; -        error.schema = schemaStack[schemaStack.length - 1].schema; -        error.valueStack = valueStack; -        error.schemaStack = schemaStack; -        return error; -    } - -    _isObject(value) { -        return typeof value === 'object' && value !== null && !Array.isArray(value); +        return new JsonSchemaError(message, valueStack, schemaStack);      } +    /** +     * @param {string} pattern +     * @param {string} flags +     * @returns {RegExp} +     */      _getRegex(pattern, flags) {          if (this._regexCache === null) {              this._regexCache = new CacheMap(100); @@ -208,81 +336,125 @@ export class JsonSchema {          return regex;      } -    _getUnconstrainedSchema() { -        return {}; -    } - -    _getObjectPropertySchemaInfo(property) { -        const {properties} = this._schema; -        if (this._isObject(properties)) { +    /** +     * @param {import('json-schema').Schema} schema +     * @param {string} property +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     */ +    _getObjectPropertySchemaInfo(schema, property) { +        if (typeof schema === 'boolean') { +            return {schema, stack: [{schema, path: null}]}; +        } +        const {properties} = schema; +        if (typeof properties !== 'undefined' && Object.prototype.hasOwnProperty.call(properties, property)) {              const propertySchema = properties[property]; -            if (this._isObject(propertySchema)) { -                return {schema: propertySchema, path: ['properties', property]}; +            if (typeof propertySchema !== 'undefined') { +                return { +                    schema: propertySchema, +                    stack: [ +                        {schema: properties, path: 'properties'}, +                        {schema: propertySchema, path: property} +                    ] +                };              }          } - -        const {additionalProperties} = this._schema; -        if (additionalProperties === false) { -            return null; -        } else if (this._isObject(additionalProperties)) { -            return {schema: additionalProperties, path: 'additionalProperties'}; -        } else { -            const result = this._getUnconstrainedSchema(); -            return {schema: result, path: null}; -        } -    } - -    _getArrayItemSchemaInfo(index) { -        const {items} = this._schema; -        if (this._isObject(items)) { -            return {schema: items, path: 'items'}; -        } -        if (Array.isArray(items)) { -            if (index >= 0 && index < items.length) { -                const propertySchema = items[index]; -                if (this._isObject(propertySchema)) { -                    return {schema: propertySchema, path: ['items', index]}; +        return this._getOptionalSchemaInfo(schema.additionalProperties, 'additionalProperties'); +    } + +    /** +     * @param {import('json-schema').Schema} schema +     * @param {number} index +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     */ +    _getArrayItemSchemaInfo(schema, index) { +        if (typeof schema === 'boolean') { +            return {schema, stack: [{schema, path: null}]}; +        } +        const {prefixItems} = schema; +        if (typeof prefixItems !== 'undefined') { +            if (index >= 0 && index < prefixItems.length) { +                const itemSchema = prefixItems[index]; +                if (typeof itemSchema !== 'undefined') { +                    return { +                        schema: itemSchema, +                        stack: [ +                            {schema: prefixItems, path: 'prefixItems'}, +                            {schema: itemSchema, path: index} +                        ] +                    };                  }              }          } - -        const {additionalItems} = this._schema; -        if (additionalItems === false) { -            return null; -        } else if (this._isObject(additionalItems)) { -            return {schema: additionalItems, path: 'additionalItems'}; -        } else { -            const result = this._getUnconstrainedSchema(); -            return {schema: result, path: null}; -        } -    } - -    _getSchemaOrValueType(value) { -        const {type} = this._schema; - -        if (Array.isArray(type)) { -            if (typeof value !== 'undefined') { -                const valueType = this._getValueType(value); -                if (type.indexOf(valueType) >= 0) { -                    return valueType; +        const {items} = schema; +        if (typeof items !== 'undefined') { +            if (Array.isArray(items)) { // Legacy schema format +                if (index >= 0 && index < items.length) { +                    const itemSchema = items[index]; +                    if (typeof itemSchema !== 'undefined') { +                        return { +                            schema: itemSchema, +                            stack: [ +                                {schema: items, path: 'items'}, +                                {schema: itemSchema, path: index} +                            ] +                        }; +                    }                  } +            } else { +                return { +                    schema: items, +                    stack: [{schema: items, path: 'items'}] +                };              } -            return null;          } +        return this._getOptionalSchemaInfo(schema.additionalItems, 'additionalItems'); +    } -        if (typeof type !== 'undefined') { return type; } -        return (typeof value !== 'undefined') ? this._getValueType(value) : null; +    /** +     * @param {import('json-schema').Schema|undefined} schema +     * @param {string|number|null} path +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     */ +    _getOptionalSchemaInfo(schema, path) { +        switch (typeof schema) { +            case 'boolean': +            case 'object': +                break; +            default: +                schema = true; +                path = null; +                break; +        } +        return {schema, stack: [{schema, path}]};      } +    /** +     * @param {unknown} value +     * @returns {?import('json-schema').Type} +     * @throws {Error} +     */      _getValueType(value) {          const type = typeof value; -        if (type === 'object') { -            if (value === null) { return 'null'; } -            if (Array.isArray(value)) { return 'array'; } +        switch (type) { +            case 'object': +                if (value === null) { return 'null'; } +                if (Array.isArray(value)) { return 'array'; } +                return 'object'; +            case 'string': +            case 'number': +            case 'boolean': +                return type; +            default: +                return null;          } -        return type;      } +    /** +     * @param {unknown} value +     * @param {?import('json-schema').Type} type +     * @param {import('json-schema').Type|import('json-schema').Type[]|undefined} schemaTypes +     * @returns {boolean} +     */      _isValueTypeAny(value, type, schemaTypes) {          if (typeof schemaTypes === 'string') {              return this._isValueType(value, type, schemaTypes); @@ -297,13 +469,24 @@ export class JsonSchema {          return true;      } +    /** +     * @param {unknown} value +     * @param {?import('json-schema').Type} type +     * @param {import('json-schema').Type} schemaType +     * @returns {boolean} +     */      _isValueType(value, type, schemaType) {          return (              type === schemaType || -            (schemaType === 'integer' && Math.floor(value) === value) +            (schemaType === 'integer' && typeof value === 'number' && Math.floor(value) === value)          );      } +    /** +     * @param {unknown} value1 +     * @param {import('json-schema').Value[]} valueList +     * @returns {boolean} +     */      _valuesAreEqualAny(value1, valueList) {          for (const value2 of valueList) {              if (this._valuesAreEqual(value1, value2)) { @@ -313,29 +496,45 @@ export class JsonSchema {          return false;      } +    /** +     * @param {unknown} value1 +     * @param {import('json-schema').Value} value2 +     * @returns {boolean} +     */      _valuesAreEqual(value1, value2) {          return value1 === value2;      } -    _getResolveSchemaInfo(schemaInfo) { -        const ref = schemaInfo.schema.$ref; -        if (typeof ref !== 'string') { return schemaInfo; } - -        const {path: basePath} = schemaInfo; -        const {schema, path} = this._getReference(ref); -        if (Array.isArray(basePath)) { -            path.unshift(...basePath); -        } else { -            path.unshift(basePath); +    /** +     * @param {import('json-schema').Schema} schema +     * @param {import('json-schema').SchemaStackItem[]} stack +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     */ +    _getResolvedSchemaInfo(schema, stack) { +        if (typeof schema !== 'boolean') { +            const ref = schema.$ref; +            if (typeof ref === 'string') { +                const {schema: schema2, stack: stack2} = this._getReference(ref); +                return { +                    schema: schema2, +                    stack: [...stack, ...stack2] +                }; +            }          } -        return {schema, path}; +        return {schema, stack};      } +    /** +     * @param {string} ref +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     * @throws {Error} +     */      _getReference(ref) {          if (!ref.startsWith('#/')) {              throw this._createError(`Unsupported reference path: ${ref}`);          } +        /** @type {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}|undefined} */          let info;          if (this._refCache !== null) {              info = this._refCache.get(ref); @@ -348,12 +547,20 @@ export class JsonSchema {              this._refCache.set(ref, info);          } -        return {schema: info.schema, path: [...info.path]}; +        info.stack = this._copySchemaStack(info.stack); +        return info;      } +    /** +     * @param {string} ref +     * @returns {{schema: import('json-schema').Schema, stack: import('json-schema').SchemaStackItem[]}} +     * @throws {Error} +     */      _getReferenceUncached(ref) { +        /** @type {Set<string>} */          const visited = new Set(); -        const path = []; +        /** @type {import('json-schema').SchemaStackItem[]} */ +        const stack = [];          while (true) {              if (visited.has(ref)) {                  throw this._createError(`Recursive reference: ${ref}`); @@ -362,106 +569,139 @@ export class JsonSchema {              const pathParts = ref.substring(2).split('/');              let schema = this._rootSchema; -            try { -                for (const pathPart of pathParts) { -                    schema = schema[pathPart]; +            stack.push({schema, path: null}); +            for (const pathPart of pathParts) { +                if (!(typeof schema === 'object' && schema !== null && Object.prototype.hasOwnProperty.call(schema, pathPart))) { +                    throw this._createError(`Invalid reference: ${ref}`);                  } -            } catch (e) { -                throw this._createError(`Invalid reference: ${ref}`); +                const schemaNext = /** @type {import('core').UnknownObject} */ (schema)[pathPart]; +                if (!(typeof schemaNext === 'boolean' || (typeof schemaNext === 'object' && schemaNext !== null))) { +                    throw this._createError(`Invalid reference: ${ref}`); +                } +                schema = schemaNext; +                stack.push({schema, path: pathPart});              } -            if (!this._isObject(schema)) { +            if (Array.isArray(schema)) {                  throw this._createError(`Invalid reference: ${ref}`);              } -            path.push(null, ...pathParts); - -            ref = schema.$ref; -            if (typeof ref !== 'string') { -                return {schema, path}; +            const refNext = typeof schema === 'object' && schema !== null ? schema.$ref : void 0; +            if (typeof refNext !== 'string') { +                return {schema, stack};              } +            ref = refNext; +        } +    } + +    /** +     * @param {import('json-schema').SchemaStackItem[]} schemaStack +     * @returns {import('json-schema').SchemaStackItem[]} +     */ +    _copySchemaStack(schemaStack) { +        /** @type {import('json-schema').SchemaStackItem[]} */ +        const results = []; +        for (const {schema, path} of schemaStack) { +            results.push({schema, path});          } +        return results;      }      // Validation -    _isValidCurrent(value) { +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     * @returns {boolean} +     */ +    _isValidCurrent(schema, value) {          try { -            this._validate(value); +            this._validate(schema, value);              return true;          } catch (e) {              return false;          }      } -    _validate(value) { +    /** +     * @param {import('json-schema').Schema} schema +     * @param {unknown} value +     */ +    _validate(schema, value) {          if (this._progress !== null) {              const counter = (this._progressCounter + 1) % this._progressInterval;              this._progressCounter = counter;              if (counter === 0) { this._progress(this); }          } -        const ref = this._schema.$ref; -        const schemaInfo = (typeof ref === 'string') ? this._getReference(ref) : null; - -        if (schemaInfo === null) { -            this._validateInner(value); -        } else { -            this._schemaPush(schemaInfo.schema, schemaInfo.path); -            try { -                this._validateInner(value); -            } finally { -                this._schemaPop(); -            } -        } -    } - -    _validateInner(value) { -        this._validateSingleSchema(value); -        this._validateConditional(value); -        this._validateAllOf(value); -        this._validateAnyOf(value); -        this._validateOneOf(value); -        this._validateNoneOf(value); -    } - -    _validateConditional(value) { -        const ifSchema = this._schema.if; -        if (!this._isObject(ifSchema)) { return; } +        const {schema: schema2, stack} = this._getResolvedSchemaInfo(schema, []); +        this._schemaPushMultiple(stack); +        try { +            this._validateInner(schema2, value); +        } finally { +            this._schemaPopMultiple(stack.length); +        } +    } + +    /** +     * @param {import('json-schema').Schema} schema +     * @param {unknown} value +     * @throws {Error} +     */ +    _validateInner(schema, value) { +        if (schema === true) { return; } +        if (schema === false) { throw this._createError('False schema'); } +        this._validateSingleSchema(schema, value); +        this._validateConditional(schema, value); +        this._validateAllOf(schema, value); +        this._validateAnyOf(schema, value); +        this._validateOneOf(schema, value); +        this._validateNot(schema, value); +    } + +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     */ +    _validateConditional(schema, value) { +        const ifSchema = schema.if; +        if (typeof ifSchema === 'undefined') { return; }          let okay = true;          this._schemaPush(ifSchema, 'if');          try { -            this._validate(value); +            this._validate(ifSchema, value);          } catch (e) {              okay = false;          } finally {              this._schemaPop();          } -        const nextSchema = okay ? this._schema.then : this._schema.else; -        if (this._isObject(nextSchema)) { return; } +        const nextSchema = okay ? schema.then : schema.else; +        if (typeof nextSchema === 'undefined') { return; }          this._schemaPush(nextSchema, okay ? 'then' : 'else');          try { -            this._validate(value); +            this._validate(nextSchema, value);          } finally {              this._schemaPop();          }      } -    _validateAllOf(value) { -        const subSchemas = this._schema.allOf; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     */ +    _validateAllOf(schema, value) { +        const subSchemas = schema.allOf;          if (!Array.isArray(subSchemas)) { return; }          this._schemaPush(subSchemas, 'allOf');          try {              for (let i = 0, ii = subSchemas.length; i < ii; ++i) {                  const subSchema = subSchemas[i]; -                if (!this._isObject(subSchema)) { continue; } -                  this._schemaPush(subSchema, i);                  try { -                    this._validate(value); +                    this._validate(subSchema, value);                  } finally {                      this._schemaPop();                  } @@ -471,19 +711,21 @@ export class JsonSchema {          }      } -    _validateAnyOf(value) { -        const subSchemas = this._schema.anyOf; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     */ +    _validateAnyOf(schema, value) { +        const subSchemas = schema.anyOf;          if (!Array.isArray(subSchemas)) { return; }          this._schemaPush(subSchemas, 'anyOf');          try {              for (let i = 0, ii = subSchemas.length; i < ii; ++i) {                  const subSchema = subSchemas[i]; -                if (!this._isObject(subSchema)) { continue; } -                  this._schemaPush(subSchema, i);                  try { -                    this._validate(value); +                    this._validate(subSchema, value);                      return;                  } catch (e) {                      // NOP @@ -498,8 +740,12 @@ export class JsonSchema {          }      } -    _validateOneOf(value) { -        const subSchemas = this._schema.oneOf; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     */ +    _validateOneOf(schema, value) { +        const subSchemas = schema.oneOf;          if (!Array.isArray(subSchemas)) { return; }          this._schemaPush(subSchemas, 'oneOf'); @@ -507,11 +753,9 @@ export class JsonSchema {              let count = 0;              for (let i = 0, ii = subSchemas.length; i < ii; ++i) {                  const subSchema = subSchemas[i]; -                if (!this._isObject(subSchema)) { continue; } -                  this._schemaPush(subSchema, i);                  try { -                    this._validate(value); +                    this._validate(subSchema, value);                      ++count;                  } catch (e) {                      // NOP @@ -528,33 +772,37 @@ export class JsonSchema {          }      } -    _validateNoneOf(value) { -        const subSchemas = this._schema.not; -        if (!Array.isArray(subSchemas)) { return; } +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     * @throws {Error} +     */ +    _validateNot(schema, value) { +        const notSchema = schema.not; +        if (typeof notSchema === 'undefined') { return; } -        this._schemaPush(subSchemas, 'not'); -        try { -            for (let i = 0, ii = subSchemas.length; i < ii; ++i) { -                const subSchema = subSchemas[i]; -                if (!this._isObject(subSchema)) { continue; } +        if (Array.isArray(notSchema)) { +            throw this._createError('not schema is an array'); +        } -                this._schemaPush(subSchema, i); -                try { -                    this._validate(value); -                } catch (e) { -                    continue; -                } finally { -                    this._schemaPop(); -                } -                throw this._createError(`not[${i}] schema matched`); -            } +        this._schemaPush(notSchema, 'not'); +        try { +            this._validate(notSchema, value); +        } catch (e) { +            return;          } finally {              this._schemaPop();          } +        throw this._createError('not schema matched');      } -    _validateSingleSchema(value) { -        const {type: schemaType, const: schemaConst, enum: schemaEnum} = this._schema; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown} value +     * @throws {Error} +     */ +    _validateSingleSchema(schema, value) { +        const {type: schemaType, const: schemaConst, enum: schemaEnum} = schema;          const type = this._getValueType(value);          if (!this._isValueTypeAny(value, type, schemaType)) {              throw this._createError(`Value type ${type} does not match schema type ${schemaType}`); @@ -570,22 +818,27 @@ export class JsonSchema {          switch (type) {              case 'number': -                this._validateNumber(value); +                this._validateNumber(schema, /** @type {number} */ (value));                  break;              case 'string': -                this._validateString(value); +                this._validateString(schema, /** @type {string} */ (value));                  break;              case 'array': -                this._validateArray(value); +                this._validateArray(schema, /** @type {import('json-schema').Value[]} */ (value));                  break;              case 'object': -                this._validateObject(value); +                this._validateObject(schema, /** @type {import('json-schema').ValueObject} */ (value));                  break;          }      } -    _validateNumber(value) { -        const {multipleOf, minimum, exclusiveMinimum, maximum, exclusiveMaximum} = this._schema; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {number} value +     * @throws {Error} +     */ +    _validateNumber(schema, value) { +        const {multipleOf, minimum, exclusiveMinimum, maximum, exclusiveMaximum} = schema;          if (typeof multipleOf === 'number' && Math.floor(value / multipleOf) * multipleOf !== value) {              throw this._createError(`Number is not a multiple of ${multipleOf}`);          } @@ -607,8 +860,13 @@ export class JsonSchema {          }      } -    _validateString(value) { -        const {minLength, maxLength, pattern} = this._schema; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {string} value +     * @throws {Error} +     */ +    _validateString(schema, value) { +        const {minLength, maxLength, pattern} = schema;          if (typeof minLength === 'number' && value.length < minLength) {              throw this._createError('String length too short');          } @@ -618,14 +876,14 @@ export class JsonSchema {          }          if (typeof pattern === 'string') { -            let {patternFlags} = this._schema; +            let {patternFlags} = schema;              if (typeof patternFlags !== 'string') { patternFlags = ''; }              let regex;              try {                  regex = this._getRegex(pattern, patternFlags);              } catch (e) { -                throw this._createError(`Pattern is invalid (${e.message})`); +                throw this._createError(`Pattern is invalid (${e instanceof Error ? e.message : `${e}`})`);              }              if (!regex.test(value)) { @@ -634,8 +892,13 @@ export class JsonSchema {          }      } -    _validateArray(value) { -        const {minItems, maxItems} = this._schema; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown[]} value +     * @throws {Error} +     */ +    _validateArray(schema, value) { +        const {minItems, maxItems} = schema;          const {length} = value;          if (typeof minItems === 'number' && length < minItems) { @@ -646,30 +909,35 @@ export class JsonSchema {              throw this._createError('Array length too long');          } -        this._validateArrayContains(value); +        this._validateArrayContains(schema, value);          for (let i = 0; i < length; ++i) { -            const schemaInfo = this._getArrayItemSchemaInfo(i); -            if (schemaInfo === null) { +            const {schema: itemSchema, stack} = this._getArrayItemSchemaInfo(schema, i); +            if (itemSchema === false) {                  throw this._createError(`No schema found for array[${i}]`);              }              const propertyValue = value[i]; -            this._schemaPush(schemaInfo.schema, schemaInfo.path); +            this._schemaPushMultiple(stack);              this._valuePush(propertyValue, i);              try { -                this._validate(propertyValue); +                this._validate(itemSchema, propertyValue);              } finally {                  this._valuePop(); -                this._schemaPop(); +                this._schemaPopMultiple(stack.length);              }          }      } -    _validateArrayContains(value) { -        const containsSchema = this._schema.contains; -        if (!this._isObject(containsSchema)) { return; } +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {unknown[]} value +     * @throws {Error} +     */ +    _validateArrayContains(schema, value) { +        const containsSchema = schema.contains; +        if (typeof containsSchema === 'undefined') { return; }          this._schemaPush(containsSchema, 'contains');          try { @@ -677,7 +945,7 @@ export class JsonSchema {                  const propertyValue = value[i];                  this._valuePush(propertyValue, i);                  try { -                    this._validate(propertyValue); +                    this._validate(containsSchema, propertyValue);                      return;                  } catch (e) {                      // NOP @@ -691,8 +959,13 @@ export class JsonSchema {          }      } -    _validateObject(value) { -        const {required, minProperties, maxProperties} = this._schema; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {import('json-schema').ValueObject} value +     * @throws {Error} +     */ +    _validateObject(schema, value) { +        const {required, minProperties, maxProperties} = schema;          const properties = Object.getOwnPropertyNames(value);          const {length} = properties; @@ -714,27 +987,32 @@ export class JsonSchema {          for (let i = 0; i < length; ++i) {              const property = properties[i]; -            const schemaInfo = this._getObjectPropertySchemaInfo(property); -            if (schemaInfo === null) { +            const {schema: propertySchema, stack} = this._getObjectPropertySchemaInfo(schema, property); +            if (propertySchema === false) {                  throw this._createError(`No schema found for ${property}`);              }              const propertyValue = value[property]; -            this._schemaPush(schemaInfo.schema, schemaInfo.path); +            this._schemaPushMultiple(stack);              this._valuePush(propertyValue, property);              try { -                this._validate(propertyValue); +                this._validate(propertySchema, propertyValue);              } finally {                  this._valuePop(); -                this._schemaPop(); +                this._schemaPopMultiple(stack.length);              }          }      }      // Creation +    /** +     * @param {import('json-schema').Type|import('json-schema').Type[]|undefined} type +     * @returns {import('json-schema').Value} +     */      _getDefaultTypeValue(type) { +        if (Array.isArray(type)) { type = type[0]; }          if (typeof type === 'string') {              switch (type) {                  case 'null': @@ -755,95 +1033,122 @@ export class JsonSchema {          return null;      } -    _getDefaultSchemaValue() { -        const {type: schemaType, default: schemaDefault} = this._schema; +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @returns {import('json-schema').Value} +     */ +    _getDefaultSchemaValue(schema) { +        const {type: schemaType, default: schemaDefault} = schema;          return (              typeof schemaDefault !== 'undefined' &&              this._isValueTypeAny(schemaDefault, this._getValueType(schemaDefault), schemaType) ? -            clone(schemaDefault) : +            JsonSchema.clone(schemaDefault) :              this._getDefaultTypeValue(schemaType)          );      } -    _getValidValueOrDefault(path, value, schemaInfo) { -        schemaInfo = this._getResolveSchemaInfo(schemaInfo); -        this._schemaPush(schemaInfo.schema, schemaInfo.path); +    /** +     * @param {import('json-schema').Schema} schema +     * @param {string|number|null} path +     * @param {unknown} value +     * @param {import('json-schema').SchemaStackItem[]} stack +     * @returns {import('json-schema').Value} +     */ +    _getValidValueOrDefault(schema, path, value, stack) { +        ({schema, stack} = this._getResolvedSchemaInfo(schema, stack)); +        this._schemaPushMultiple(stack);          this._valuePush(value, path);          try { -            return this._getValidValueOrDefaultInner(value); +            return this._getValidValueOrDefaultInner(schema, value);          } finally {              this._valuePop(); -            this._schemaPop(); +            this._schemaPopMultiple(stack.length);          }      } -    _getValidValueOrDefaultInner(value) { +    /** +     * @param {import('json-schema').Schema} schema +     * @param {unknown} value +     * @returns {import('json-schema').Value} +     */ +    _getValidValueOrDefaultInner(schema, value) {          let type = this._getValueType(value); -        if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, this._schema.type)) { -            value = this._getDefaultSchemaValue(); +        if (typeof schema === 'boolean') { +            return type !== null ? /** @type {import('json-schema').ValueObject} */ (value) : null; +        } +        if (typeof value === 'undefined' || !this._isValueTypeAny(value, type, schema.type)) { +            value = this._getDefaultSchemaValue(schema);              type = this._getValueType(value);          }          switch (type) {              case 'object': -                value = this._populateObjectDefaults(value); -                break; +                return this._populateObjectDefaults(schema, /** @type {import('json-schema').ValueObject} */ (value));              case 'array': -                value = this._populateArrayDefaults(value); -                break; +                return this._populateArrayDefaults(schema, /** @type {import('json-schema').Value[]} */ (value));              default: -                if (!this._isValidCurrent(value)) { -                    const schemaDefault = this._getDefaultSchemaValue(); -                    if (this._isValidCurrent(schemaDefault)) { -                        value = schemaDefault; +                if (!this._isValidCurrent(schema, value)) { +                    const schemaDefault = this._getDefaultSchemaValue(schema); +                    if (this._isValidCurrent(schema, schemaDefault)) { +                        return schemaDefault;                      }                  }                  break;          } -        return value; +        return /** @type {import('json-schema').ValueObject} */ (value);      } -    _populateObjectDefaults(value) { +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {import('json-schema').ValueObject} value +     * @returns {import('json-schema').ValueObject} +     */ +    _populateObjectDefaults(schema, value) {          const properties = new Set(Object.getOwnPropertyNames(value)); -        const {required} = this._schema; +        const {required} = schema;          if (Array.isArray(required)) {              for (const property of required) {                  properties.delete(property); -                const schemaInfo = this._getObjectPropertySchemaInfo(property); -                if (schemaInfo === null) { continue; } +                const {schema: propertySchema, stack} = this._getObjectPropertySchemaInfo(schema, property); +                if (propertySchema === false) { continue; }                  const propertyValue = Object.prototype.hasOwnProperty.call(value, property) ? value[property] : void 0; -                value[property] = this._getValidValueOrDefault(property, propertyValue, schemaInfo); +                value[property] = this._getValidValueOrDefault(propertySchema, property, propertyValue, stack);              }          }          for (const property of properties) { -            const schemaInfo = this._getObjectPropertySchemaInfo(property); -            if (schemaInfo === null) { +            const {schema: propertySchema, stack} = this._getObjectPropertySchemaInfo(schema, property); +            if (propertySchema === false) {                  Reflect.deleteProperty(value, property);              } else { -                value[property] = this._getValidValueOrDefault(property, value[property], schemaInfo); +                value[property] = this._getValidValueOrDefault(propertySchema, property, value[property], stack);              }          }          return value;      } -    _populateArrayDefaults(value) { +    /** +     * @param {import('json-schema').SchemaObject} schema +     * @param {import('json-schema').Value[]} value +     * @returns {import('json-schema').Value[]} +     */ +    _populateArrayDefaults(schema, value) {          for (let i = 0, ii = value.length; i < ii; ++i) { -            const schemaInfo = this._getArrayItemSchemaInfo(i); -            if (schemaInfo === null) { continue; } +            const {schema: itemSchema, stack} = this._getArrayItemSchemaInfo(schema, i); +            if (itemSchema === false) { continue; }              const propertyValue = value[i]; -            value[i] = this._getValidValueOrDefault(i, propertyValue, schemaInfo); +            value[i] = this._getValidValueOrDefault(itemSchema, i, propertyValue, stack);          } -        const {minItems, maxItems} = this._schema; +        const {minItems, maxItems} = schema;          if (typeof minItems === 'number' && value.length < minItems) {              for (let i = value.length; i < minItems; ++i) { -                const schemaInfo = this._getArrayItemSchemaInfo(i); -                if (schemaInfo === null) { break; } -                const item = this._getValidValueOrDefault(i, void 0, schemaInfo); +                const {schema: itemSchema, stack} = this._getArrayItemSchemaInfo(schema, i); +                if (itemSchema === false) { break; } +                const item = this._getValidValueOrDefault(itemSchema, i, void 0, stack);                  value.push(item);              }          } @@ -856,115 +1161,187 @@ export class JsonSchema {      }  } +/** + * @implements {ProxyHandler<import('json-schema').ValueObjectOrArray>} + */  class JsonSchemaProxyHandler { -    constructor(schema) { -        this._schema = schema; +    /** +     * @param {JsonSchema} schemaValidator +     */ +    constructor(schemaValidator) { +        /** @type {JsonSchema} */ +        this._schemaValidator = schemaValidator; +        /** @type {RegExp} */          this._numberPattern = /^(?:0|[1-9]\d*)$/;      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @returns {?import('core').UnknownObject} +     */      getPrototypeOf(target) {          return Object.getPrototypeOf(target);      } +    /** +     * @type {(target: import('json-schema').ValueObjectOrArray, newPrototype: ?unknown) => boolean} +     */      setPrototypeOf() {          throw new Error('setPrototypeOf not supported');      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @returns {boolean} +     */      isExtensible(target) {          return Object.isExtensible(target);      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @returns {boolean} +     */      preventExtensions(target) {          Object.preventExtensions(target);          return true;      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @param {string|symbol} property +     * @returns {PropertyDescriptor|undefined} +     */      getOwnPropertyDescriptor(target, property) {          return Object.getOwnPropertyDescriptor(target, property);      } +    /** +     * @type {(target: import('json-schema').ValueObjectOrArray, property: string|symbol, attributes: PropertyDescriptor) => boolean} +     */      defineProperty() {          throw new Error('defineProperty not supported');      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @param {string|symbol} property +     * @returns {boolean} +     */      has(target, property) {          return property in target;      } -    get(target, property) { -        if (typeof property === 'symbol') { return target[property]; } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @param {string|symbol} property +     * @param {import('core').SafeAny} _receiver +     * @returns {import('core').SafeAny} +     */ +    get(target, property, _receiver) { +        if (typeof property === 'symbol') { return /** @type {import('core').UnknownObject} */ (target)[property]; }          let propertySchema;          if (Array.isArray(target)) {              const index = this._getArrayIndex(property);              if (index === null) {                  // Note: this does not currently wrap mutating functions like push, pop, shift, unshift, splice -                return target[property]; +                return /** @type {import('core').SafeAny} */ (target)[property];              } -            property = index; -            propertySchema = this._schema.getArrayItemSchema(property); +            property = `${index}`; +            propertySchema = this._schemaValidator.getArrayItemSchema(index);          } else { -            propertySchema = this._schema.getObjectPropertySchema(property); +            propertySchema = this._schemaValidator.getObjectPropertySchema(property);          }          if (propertySchema === null) { return void 0; } -        const value = target[property]; -        return value !== null && typeof value === 'object' ? propertySchema.createProxy(value) : value; +        const value = /** @type {import('core').UnknownObject} */ (target)[property]; +        return value !== null && typeof value === 'object' ? propertySchema.createProxy(/** @type {import('json-schema').Value} */ (value)) : value;      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @param {string|number|symbol} property +     * @param {import('core').SafeAny} value +     * @returns {boolean} +     * @throws {Error} +     */      set(target, property, value) { -        if (typeof property === 'symbol') { throw new Error(`Cannot assign symbol property ${property}`); } +        if (typeof property === 'symbol') { throw new Error(`Cannot assign symbol property ${typeof property === 'symbol' ? '<symbol>' : property}`); }          let propertySchema;          if (Array.isArray(target)) {              const index = this._getArrayIndex(property);              if (index === null) { -                target[property] = value; +                /** @type {import('core').SafeAny} */ (target)[property] = value;                  return true;              }              if (index > target.length) { throw new Error('Array index out of range'); }              property = index; -            propertySchema = this._schema.getArrayItemSchema(property); +            propertySchema = this._schemaValidator.getArrayItemSchema(property);          } else { -            propertySchema = this._schema.getObjectPropertySchema(property); +            if (typeof property !== 'string') { +                property = `${property}`; +            } +            propertySchema = this._schemaValidator.getObjectPropertySchema(property);          }          if (propertySchema === null) { throw new Error(`Property ${property} not supported`); } -        value = clone(value); +        value = JsonSchema.clone(value);          propertySchema.validate(value); -        target[property] = value; +        /** @type {import('core').UnknownObject} */ (target)[property] = value;          return true;      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @param {string|symbol} property +     * @returns {boolean} +     * @throws {Error} +     */      deleteProperty(target, property) {          const required = (              (typeof target === 'object' && target !== null) ? -            (!Array.isArray(target) && this._schema.isObjectPropertyRequired(property)) : +            (!Array.isArray(target) && typeof property === 'string' && this._schemaValidator.isObjectPropertyRequired(property)) :              true          );          if (required) { -            throw new Error(`${property} cannot be deleted`); +            throw new Error(`${typeof property === 'symbol' ? '<symbol>' : property} cannot be deleted`);          }          return Reflect.deleteProperty(target, property);      } +    /** +     * @param {import('json-schema').ValueObjectOrArray} target +     * @returns {ArrayLike<string|symbol>} +     */      ownKeys(target) {          return Reflect.ownKeys(target);      } +    /** +     * @type {(target: import('json-schema').ValueObjectOrArray, thisArg: import('core').SafeAny, argArray: import('core').SafeAny[]) => import('core').SafeAny} +     */      apply() {          throw new Error('apply not supported');      } +    /** +     * @type {(target: import('json-schema').ValueObjectOrArray, argArray: import('core').SafeAny[], newTarget: import('core').SafeFunction) => import('json-schema').ValueObjectOrArray} +     */      construct() {          throw new Error('construct not supported');      }      // Private +    /** +     * @param {string|symbol|number} property +     * @returns {?number} +     */      _getArrayIndex(property) {          if (typeof property === 'string' && this._numberPattern.test(property)) {              return Number.parseInt(property, 10); diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js index 3858cb55..70c1622f 100644 --- a/ext/js/data/options-util.js +++ b/ext/js/data/options-util.js @@ -22,20 +22,29 @@ import {JsonSchema} from './json-schema.js';  export class OptionsUtil {      constructor() { +        /** @type {?TemplatePatcher} */          this._templatePatcher = null; +        /** @type {?JsonSchema} */          this._optionsSchema = null;      } +    /** */      async prepare() { -        const schema = await this._fetchAsset('/data/schemas/options-schema.json', true); +        const schema = /** @type {import('json-schema').Schema} */ (await this._fetchJson('/data/schemas/options-schema.json'));          this._optionsSchema = new JsonSchema(schema);      } -    async update(options, targetVersion=null) { +    /** +     * @param {unknown} optionsInput +     * @param {?number} [targetVersion] +     * @returns {Promise<import('settings').Options>} +     */ +    async update(optionsInput, targetVersion=null) {          // Invalid options -        if (!isObject(options)) { -            options = {}; -        } +        let options = /** @type {{[key: string]: unknown}} */ ( +            typeof optionsInput === 'object' && optionsInput !== null && !Array.isArray(optionsInput) ? +            optionsInput : {} +        );          // Check for legacy options          let defaultProfileOptions = {}; @@ -50,7 +59,7 @@ export class OptionsUtil {          }          // Remove invalid profiles -        const profiles = options.profiles; +        const profiles = /** @type {unknown[]} */ (options.profiles);          for (let i = profiles.length - 1; i >= 0; --i) {              if (!isObject(profiles[i])) {                  profiles.splice(i, 1); @@ -87,12 +96,12 @@ export class OptionsUtil {          options = await this._applyUpdates(options, this._getVersionUpdates(targetVersion));          // Validation -        options = this._optionsSchema.getValidValueOrDefault(options); - -        // Result -        return options; +        return /** @type {import('settings').Options} */ (/** @type {JsonSchema} */ (this._optionsSchema).getValidValueOrDefault(options));      } +    /** +     * @returns {Promise<import('settings').Options>} +     */      async load() {          let options;          try { @@ -121,6 +130,10 @@ export class OptionsUtil {          return options;      } +    /** +     * @param {import('settings').Options} options +     * @returns {Promise<void>} +     */      save(options) {          return new Promise((resolve, reject) => {              chrome.storage.local.set({options: JSON.stringify(options)}, () => { @@ -134,23 +147,36 @@ export class OptionsUtil {          });      } +    /** +     * @returns {import('settings').Options} +     */      getDefault() { -        const optionsVersion = this._getVersionUpdates().length; -        const options = this._optionsSchema.getValidValueOrDefault(); +        const optionsVersion = this._getVersionUpdates(null).length; +        const options = /** @type {import('settings').Options} */ (/** @type {JsonSchema} */ (this._optionsSchema).getValidValueOrDefault());          options.version = optionsVersion;          return options;      } +    /** +     * @param {import('settings').Options} options +     * @returns {import('settings').Options} +     */      createValidatingProxy(options) { -        return this._optionsSchema.createProxy(options); +        return /** @type {import('settings').Options} */ (/** @type {JsonSchema} */ (this._optionsSchema).createProxy(options));      } +    /** +     * @param {import('settings').Options} options +     */      validate(options) { -        return this._optionsSchema.validate(options); +        /** @type {JsonSchema} */ (this._optionsSchema).validate(options);      }      // Legacy profile updating +    /** +     * @returns {(?import('options-util').LegacyUpdateFunction)[]} +     */      _legacyProfileUpdateGetUpdates() {          return [              null, @@ -242,6 +268,9 @@ export class OptionsUtil {          ];      } +    /** +     * @returns {import('options-util').LegacyOptions} +     */      _legacyProfileUpdateGetDefaults() {          return {              general: { @@ -341,9 +370,17 @@ export class OptionsUtil {          };      } +    /** +     * @param {import('options-util').IntermediateOptions} options +     * @returns {import('options-util').IntermediateOptions} +     */      _legacyProfileUpdateAssignDefaults(options) {          const defaults = this._legacyProfileUpdateGetDefaults(); +        /** +         * @param {import('options-util').IntermediateOptions} target +         * @param {import('core').UnknownObject} source +         */          const combine = (target, source) => {              for (const key in source) {                  if (!Object.prototype.hasOwnProperty.call(target, key)) { @@ -362,6 +399,10 @@ export class OptionsUtil {          return options;      } +    /** +     * @param {import('options-util').IntermediateOptions} options +     * @returns {import('options-util').IntermediateOptions} +     */      _legacyProfileUpdateUpdateVersion(options) {          const updates = this._legacyProfileUpdateGetUpdates();          this._legacyProfileUpdateAssignDefaults(options); @@ -384,6 +425,10 @@ export class OptionsUtil {      // Private +    /** +     * @param {import('options-util').IntermediateOptions} options +     * @param {string} modificationsUrl +     */      async _applyAnkiFieldTemplatesPatch(options, modificationsUrl) {          let patch = null;          for (const {options: profileOptions} of options.profiles) { @@ -391,18 +436,22 @@ export class OptionsUtil {              if (fieldTemplates === null) { continue; }              if (patch === null) { -                const content = await this._fetchAsset(modificationsUrl); +                const content = await this._fetchText(modificationsUrl);                  if (this._templatePatcher === null) {                      this._templatePatcher = new TemplatePatcher();                  }                  patch = this._templatePatcher.parsePatch(content);              } -            profileOptions.anki.fieldTemplates = this._templatePatcher.applyPatch(fieldTemplates, patch); +            profileOptions.anki.fieldTemplates = /** @type {TemplatePatcher} */ (this._templatePatcher).applyPatch(fieldTemplates, patch);          }      } -    async _fetchAsset(url, json=false) { +    /** +     * @param {string} url +     * @returns {Promise<Response>} +     */ +    async _fetchGeneric(url) {          url = chrome.runtime.getURL(url);          const response = await fetch(url, {              method: 'GET', @@ -415,9 +464,31 @@ export class OptionsUtil {          if (!response.ok) {              throw new Error(`Failed to fetch ${url}: ${response.status}`);          } -        return await (json ? response.json() : response.text()); +        return response; +    } + +    /** +     * @param {string} url +     * @returns {Promise<string>} +     */ +    async _fetchText(url) { +        const response = await this._fetchGeneric(url); +        return await response.text(); +    } + +    /** +     * @param {string} url +     * @returns {Promise<unknown>} +     */ +    async _fetchJson(url) { +        const response = await this._fetchGeneric(url); +        return await response.json();      } +    /** +     * @param {string} string +     * @returns {number} +     */      _getStringHashCode(string) {          let hashCode = 0; @@ -431,6 +502,11 @@ export class OptionsUtil {          return hashCode;      } +    /** +     * @param {import('options-util').IntermediateOptions} options +     * @param {import('options-util').ModernUpdate[]} updates +     * @returns {Promise<import('settings').Options>} +     */      async _applyUpdates(options, updates) {          const targetVersion = updates.length;          let currentVersion = options.version; @@ -449,6 +525,10 @@ export class OptionsUtil {          return options;      } +    /** +     * @param {?number} targetVersion +     * @returns {import('options-util').ModernUpdate[]} +     */      _getVersionUpdates(targetVersion) {          const result = [              {async: false, update: this._updateVersion1.bind(this)}, @@ -479,6 +559,9 @@ export class OptionsUtil {          return result;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion1(options) {          // Version 1 changes:          //  Added options.global.database.prefixWildcardsSupported = false. @@ -490,6 +573,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion2(options) {          // Version 2 changes:          //  Legacy profile update process moved into this upgrade function. @@ -502,6 +588,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionAsync} +     */      async _updateVersion3(options) {          // Version 3 changes:          //  Pitch accent Anki field templates added. @@ -509,6 +598,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionAsync} +     */      async _updateVersion4(options) {          // Version 4 changes:          //  Options conditions converted to string representations. @@ -594,6 +686,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion5(options) {          // Version 5 changes:          //  Removed legacy version number from profile options. @@ -603,6 +698,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionAsync} +     */      async _updateVersion6(options) {          // Version 6 changes:          //  Updated handlebars templates to include "conjugation" definition. @@ -625,6 +723,10 @@ export class OptionsUtil {          return options;      } +    /** +     * @param {string} templates +     * @returns {string} +     */      _updateVersion6AnkiTemplatesCompactTags(templates) {          const rawPattern1 = '{{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}';          const pattern1 = new RegExp(`((\r?\n)?[ \t]*)${escapeRegExp(rawPattern1)}`, 'g'); @@ -649,6 +751,9 @@ export class OptionsUtil {          return templates;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion7(options) {          // Version 7 changes:          //  Added general.maximumClipboardSearchLength. @@ -666,6 +771,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionAsync} +     */      async _updateVersion8(options) {          // Version 8 changes:          //  Added translation.textReplacements. @@ -755,6 +863,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion9(options) {          // Version 9 changes:          //  Added general.frequencyDisplayMode. @@ -766,6 +877,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionAsync} +     */      async _updateVersion10(options) {          // Version 10 changes:          //  Removed global option useSettingsV2. @@ -803,6 +917,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion11(options) {          // Version 11 changes:          //  Changed dictionaries to an array. @@ -827,6 +944,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionAsync} +     */      async _updateVersion12(options) {          // Version 12 changes:          //  Changed sentenceParsing.enableTerminationCharacters to sentenceParsing.terminationCharacterMode. @@ -841,7 +961,7 @@ export class OptionsUtil {              delete sentenceParsing.enableTerminationCharacters;              const {sources, customSourceUrl, customSourceType, textToSpeechVoice} = audio; -            audio.sources = sources.map((type) => { +            audio.sources = /** @type {string[]} */ (sources).map((type) => {                  switch (type) {                      case 'text-to-speech':                      case 'text-to-speech-reading': @@ -859,6 +979,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionAsync} +     */      async _updateVersion13(options) {          // Version 13 changes:          //  Handlebars templates updated to use formatGlossary. @@ -874,6 +997,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion14(options) {          // Version 14 changes:          //  Added accessibility options. @@ -885,6 +1011,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion15(options) {          // Version 15 changes:          //  Added general.sortFrequencyDictionary. @@ -896,6 +1025,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion16(options) {          // Version 16 changes:          //  Added scanning.matchTypePrefix. @@ -905,12 +1037,16 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion17(options) {          // Version 17 changes:          //  Added vertical sentence punctuation to terminationCharacters.          const additions = ['︒', '︕', '︖', '︙'];          for (const profile of options.profiles) { -            const {terminationCharacters} = profile.options.sentenceParsing; +            /** @type {import('settings').SentenceParsingTerminationCharacterOption[]} */ +            const terminationCharacters = profile.options.sentenceParsing.terminationCharacters;              const newAdditions = [];              for (const character of additions) {                  if (terminationCharacters.findIndex((value) => (value.character1 === character && value.character2 === null)) < 0) { @@ -930,6 +1066,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion18(options) {          // Version 18 changes:          //  general.popupTheme's 'default' value changed to 'light' @@ -952,6 +1091,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion19(options) {          // Version 19 changes:          //  Added anki.noteGuiMode. @@ -979,6 +1121,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionSync} +     */      _updateVersion20(options) {          // Version 20 changes:          //  Added anki.downloadTimeout. @@ -999,6 +1144,9 @@ export class OptionsUtil {          return options;      } +    /** +     * @type {import('options-util').ModernUpdateFunctionAsync} +     */      async _updateVersion21(options) {          await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v21.handlebars'); @@ -1018,6 +1166,10 @@ export class OptionsUtil {          return options;      } +    /** +     * @param {string} url +     * @returns {Promise<chrome.tabs.Tab>} +     */      _createTab(url) {          return new Promise((resolve, reject) => {              chrome.tabs.create({url}, (tab) => { diff --git a/ext/js/data/permissions-util.js b/ext/js/data/permissions-util.js index d645f21e..76c5031b 100644 --- a/ext/js/data/permissions-util.js +++ b/ext/js/data/permissions-util.js @@ -20,12 +20,17 @@ import {AnkiUtil} from './anki-util.js';  export class PermissionsUtil {      constructor() { +        /** @type {Set<string>} */          this._ankiFieldMarkersRequiringClipboardPermission = new Set([              'clipboard-image',              'clipboard-text'          ]);      } +    /** +     * @param {chrome.permissions.Permissions} permissions +     * @returns {Promise<boolean>} +     */      hasPermissions(permissions) {          return new Promise((resolve, reject) => chrome.permissions.contains(permissions, (result) => {              const e = chrome.runtime.lastError; @@ -37,6 +42,11 @@ export class PermissionsUtil {          }));      } +    /** +     * @param {chrome.permissions.Permissions} permissions +     * @param {boolean} shouldHave +     * @returns {Promise<boolean>} +     */      setPermissionsGranted(permissions, shouldHave) {          return (              shouldHave ? @@ -59,6 +69,9 @@ export class PermissionsUtil {          );      } +    /** +     * @returns {Promise<chrome.permissions.Permissions>} +     */      getAllPermissions() {          return new Promise((resolve, reject) => chrome.permissions.getAll((result) => {              const e = chrome.runtime.lastError; @@ -70,6 +83,10 @@ export class PermissionsUtil {          }));      } +    /** +     * @param {string} fieldValue +     * @returns {string[]} +     */      getRequiredPermissionsForAnkiFieldValue(fieldValue) {          const markers = AnkiUtil.getFieldMarkers(fieldValue);          const markerPermissions = this._ankiFieldMarkersRequiringClipboardPermission; @@ -81,6 +98,11 @@ export class PermissionsUtil {          return [];      } +    /** +     * @param {chrome.permissions.Permissions} permissions +     * @param {import('settings').ProfileOptions} options +     * @returns {boolean} +     */      hasRequiredPermissionsForOptions(permissions, options) {          const permissionsSet = new Set(permissions.permissions); diff --git a/ext/js/data/sandbox/anki-note-data-creator.js b/ext/js/data/sandbox/anki-note-data-creator.js index 371a62a2..dce71938 100644 --- a/ext/js/data/sandbox/anki-note-data-creator.js +++ b/ext/js/data/sandbox/anki-note-data-creator.js @@ -25,24 +25,18 @@ import {DictionaryDataUtil} from '../../language/sandbox/dictionary-data-util.js  export class AnkiNoteDataCreator {      /**       * Creates a new instance. -     * @param {JapaneseUtil} japaneseUtil An instance of `JapaneseUtil`. +     * @param {import('../../language/sandbox/japanese-util.js').JapaneseUtil} japaneseUtil An instance of `JapaneseUtil`.       */      constructor(japaneseUtil) { +        /** @type {import('../../language/sandbox/japanese-util.js').JapaneseUtil} */          this._japaneseUtil = japaneseUtil;      }      /**       * Creates a compatibility representation of the specified data.       * @param {string} marker The marker that is being used for template rendering. -     * @param {object} details Information which is used to generate the data. -     * @param {Translation.DictionaryEntry} details.dictionaryEntry The dictionary entry. -     * @param {string} details.resultOutputMode The result output mode. -     * @param {string} details.mode The mode being used to generate the Anki data. -     * @param {string} details.glossaryLayoutMode The glossary layout mode. -     * @param {boolean} details.compactTags Whether or not compact tags mode is enabled. -     * @param {{documentTitle: string, query: string, fullQuery: string}} details.context Contextual information about the source of the dictionary entry. -     * @param {object} details.media Media data. -     * @returns {object} An object used for rendering Anki templates. +     * @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, @@ -53,6 +47,7 @@ export class AnkiNoteDataCreator {          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)); @@ -60,7 +55,18 @@ export class AnkiNoteDataCreator {          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)); -        if (typeof media !== 'object' || media === null || Array.isArray(media)) { media = {}; } +        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); }, @@ -77,7 +83,8 @@ export class AnkiNoteDataCreator {              get pitches() { return self.getCachedValue(pitches); },              get pitchCount() { return self.getCachedValue(pitchCount); },              get context() { return self.getCachedValue(context2); }, -            media +            media, +            dictionaryEntry          };          Object.defineProperty(result, 'dictionaryEntry', {              configurable: false, @@ -90,8 +97,9 @@ export class AnkiNoteDataCreator {      /**       * Creates a deferred-evaluation value. -     * @param {Function} getter The function to invoke to get the return value. -     * @returns {{getter: Function, hasValue: false, value: undefined}} An object which can be passed into `getCachedValue`. +     * @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}; @@ -99,11 +107,12 @@ export class AnkiNoteDataCreator {      /**       * Gets the value of a cached object. -     * @param {{getter: Function, hasValue: boolean, value: *}} item An object that was returned from `createCachedValue`. -     * @returns {*} The result of evaluating the getter, which is cached after the first invocation. +     * @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 item.value; } +        if (item.hasValue) { return /** @type {T} */ (item.value); }          const value = item.getter();          item.value = value;          item.hasValue = true; @@ -112,10 +121,10 @@ export class AnkiNoteDataCreator {      // Private -    _asObject(value) { -        return (typeof value === 'object' && value !== null ? value : {}); -    } - +    /** +     * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry +     * @returns {?import('dictionary').TermSource} +     */      _getPrimarySource(dictionaryEntry) {          for (const headword of dictionaryEntry.headwords) {              for (const source of headword.sources) { @@ -125,6 +134,10 @@ export class AnkiNoteDataCreator {          return null;      } +    /** +     * @param {import('dictionary').DictionaryEntry} dictionaryEntry +     * @returns {string[]} +     */      _getUniqueExpressions(dictionaryEntry) {          if (dictionaryEntry.type === 'term') {              const results = new Set(); @@ -137,6 +150,10 @@ export class AnkiNoteDataCreator {          }      } +    /** +     * @param {import('dictionary').DictionaryEntry} dictionaryEntry +     * @returns {string[]} +     */      _getUniqueReadings(dictionaryEntry) {          if (dictionaryEntry.type === 'term') {              const results = new Set(); @@ -149,8 +166,12 @@ export class AnkiNoteDataCreator {          }      } +    /** +     * @param {import('anki-templates-internal').Context} context +     * @returns {import('anki-templates').Context} +     */      _getPublicContext(context) { -        let {documentTitle, query, fullQuery} = this._asObject(context); +        let {documentTitle, query, fullQuery} = context;          if (typeof documentTitle !== 'string') { documentTitle = ''; }          return {              query, @@ -161,10 +182,16 @@ export class AnkiNoteDataCreator {          };      } +    /** +     * @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 {terms, reading, position, nasalPositions, devoicePositions, tags, exclusiveTerms, exclusiveReadings} of pronunciations) {                      pitches.push({ @@ -173,7 +200,7 @@ export class AnkiNoteDataCreator {                          position,                          nasalPositions,                          devoicePositions, -                        tags, +                        tags: this._convertPitchTags(tags),                          exclusiveExpressions: exclusiveTerms,                          exclusiveReadings                      }); @@ -184,11 +211,21 @@ export class AnkiNoteDataCreator {          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('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': @@ -196,16 +233,22 @@ export class AnkiNoteDataCreator {              case 'kanji':                  return this._getKanjiDefinition(dictionaryEntry, context);              default: -                return {}; +                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; -        let {url} = this._asObject(context); +        let {url} = context;          if (typeof url !== 'string') { url = ''; }          const stats = this.createCachedValue(this._getKanjiStats.bind(this, dictionaryEntry)); @@ -228,14 +271,24 @@ export class AnkiNoteDataCreator {          };      } +    /** +     * @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(this._convertKanjiStat.bind(this)); +            results[key] = value.map(convertKanjiStatBind);          }          return results;      } +    /** +     * @param {import('dictionary').KanjiStat} kanjiStat +     * @returns {import('anki-templates').KanjiStat} +     */      _convertKanjiStat({name, category, content, order, score, dictionary, value}) {          return {              name, @@ -248,7 +301,12 @@ export class AnkiNoteDataCreator {          };      } +    /** +     * @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({ @@ -265,9 +323,17 @@ export class AnkiNoteDataCreator {          return results;      } +    /** +     * @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; @@ -276,7 +342,7 @@ export class AnkiNoteDataCreator {          const {inflections, score, dictionaryIndex, dictionaryPriority, sourceTermExactMatchCount, definitions} = dictionaryEntry; -        let {url} = this._asObject(context); +        let {url} = context;          if (typeof url !== 'string') { url = ''; }          const primarySource = this._getPrimarySource(dictionaryEntry); @@ -330,6 +396,10 @@ export class AnkiNoteDataCreator {          };      } +    /** +     * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry +     * @returns {string[]} +     */      _getTermDictionaryNames(dictionaryEntry) {          const dictionaryNames = new Set();          for (const {dictionary} of dictionaryEntry.definitions) { @@ -338,11 +408,18 @@ export class AnkiNoteDataCreator {          return [...dictionaryNames];      } +    /** +     * @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); @@ -351,7 +428,9 @@ export class AnkiNoteDataCreator {          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 = []; @@ -378,6 +457,10 @@ export class AnkiNoteDataCreator {          };      } +    /** +     * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry +     * @returns {import('anki-templates').TermFrequency[]} +     */      _getTermFrequencies(dictionaryEntry) {          const results = [];          const {headwords} = dictionaryEntry; @@ -400,7 +483,12 @@ export class AnkiNoteDataCreator {          return results;      } +    /** +     * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry +     * @returns {import('anki-templates').TermPronunciation[]} +     */      _getTermPitches(dictionaryEntry) { +        // eslint-disable-next-line @typescript-eslint/no-this-alias          const self = this;          const results = [];          const {headwords} = dictionaryEntry; @@ -423,7 +511,12 @@ export class AnkiNoteDataCreator {          return results;      } +    /** +     * @param {import('dictionary').TermPitch[]} pitches +     * @returns {import('anki-templates').TermPitch[]} +     */      _getTermPitchesInner(pitches) { +        // eslint-disable-next-line @typescript-eslint/no-this-alias          const self = this;          const results = [];          for (const {position, tags} of pitches) { @@ -436,7 +529,12 @@ export class AnkiNoteDataCreator {          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; @@ -463,6 +561,11 @@ export class AnkiNoteDataCreator {          return results;      } +    /** +     * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry +     * @param {number} i +     * @returns {import('anki-templates').TermFrequency[]} +     */      _getTermExpressionFrequencies(dictionaryEntry, i) {          const results = [];          const {headwords, frequencies} = dictionaryEntry; @@ -486,7 +589,13 @@ export class AnkiNoteDataCreator {          return results;      } +    /** +     * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry +     * @param {number} i +     * @returns {import('anki-templates').TermPronunciation[]} +     */      _getTermExpressionPitches(dictionaryEntry, i) { +        // eslint-disable-next-line @typescript-eslint/no-this-alias          const self = this;          const results = [];          const {headwords, pronunciations} = dictionaryEntry; @@ -510,11 +619,20 @@ export class AnkiNoteDataCreator {          return results;      } +    /** +     * @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 = []; @@ -526,6 +644,11 @@ export class AnkiNoteDataCreator {          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 = []; @@ -537,6 +660,10 @@ export class AnkiNoteDataCreator {          return void 0;      } +    /** +     * @param {import('dictionary').Tag[]} tags +     * @returns {import('anki-templates').Tag[]} +     */      _convertTags(tags) {          const results = [];          for (const tag of tags) { @@ -545,6 +672,10 @@ export class AnkiNoteDataCreator {          return results;      } +    /** +     * @param {import('dictionary').Tag} tag +     * @returns {import('anki-templates').Tag} +     */      _convertTag({name, category, content, order, score, dictionaries, redundant}) {          return {              name, @@ -557,6 +688,39 @@ export class AnkiNoteDataCreator {          };      } +    /** +     * @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').PitchTag} +     */ +    _convertPitchTag({name, category, content, order, score, dictionaries, redundant}) { +        return { +            name, +            category, +            order, +            score, +            content: [...content], +            dictionaries: [...dictionaries], +            redundant +        }; +    } + +    /** +     * @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) { @@ -571,8 +735,12 @@ export class AnkiNoteDataCreator {                  break;          } -        const {sentence} = this._asObject(context); -        let {text, offset} = this._asObject(sentence); +        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; } @@ -584,6 +752,11 @@ export class AnkiNoteDataCreator {          };      } +    /** +     * @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) { @@ -593,7 +766,13 @@ export class AnkiNoteDataCreator {          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 this._japaneseUtil.distributeFurigana(term, reading)) {              result.push({text, furigana: reading2}); @@ -601,11 +780,15 @@ export class AnkiNoteDataCreator {          return result;      } +    /** +     * @param {import('dictionary').TermDictionaryEntry} dictionaryEntry +     * @returns {number} +     */      _getTermDictionaryEntrySequence(dictionaryEntry) {          let hasSequence = false;          let mainSequence = -1; -        for (const {sequences, isPrimary} of dictionaryEntry.definitions) { -            if (!isPrimary) { continue; } +        if (!dictionaryEntry.isPrimary) { return mainSequence; } +        for (const {sequences} of dictionaryEntry.definitions) {              const sequence = sequences[0];              if (!hasSequence) {                  mainSequence = sequence; |