summaryrefslogtreecommitdiff
path: root/ext/js/data
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/data')
-rw-r--r--ext/js/data/anki-note-builder.js143
-rw-r--r--ext/js/data/anki-util.js2
-rw-r--r--ext/js/data/database.js206
-rw-r--r--ext/js/data/json-schema.js927
-rw-r--r--ext/js/data/options-util.js185
-rw-r--r--ext/js/data/permissions-util.js22
-rw-r--r--ext/js/data/sandbox/anki-note-data-creator.js243
7 files changed, 1365 insertions, 363 deletions
diff --git a/ext/js/data/anki-note-builder.js b/ext/js/data/anki-note-builder.js
index 398036c0..f8296ee0 100644
--- a/ext/js/data/anki-note-builder.js
+++ b/ext/js/data/anki-note-builder.js
@@ -16,20 +16,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import {deferPromise, deserializeError} from '../core.js';
+import {deferPromise} from '../core.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: JapaneseUtil}} details
+ */
constructor({japaneseUtil}) {
+ /** @type {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 +68,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 +88,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 +102,7 @@ export class AnkiNoteBuilder {
}
}
+ /** @type {import('anki').Note} */
const note = {
fields: noteFields,
tags,
@@ -108,6 +121,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 +132,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 +171,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 +193,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 +220,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 +242,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 +258,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 +278,9 @@ export class AnkiNoteBuilder {
return promise;
}
+ /**
+ * @returns {void}
+ */
_runBatchedRequestsDelayed() {
if (this._batchedRequestsQueued) { return; }
this._batchedRequestsQueued = true;
@@ -229,20 +290,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 +323,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 +344,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 +354,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 +369,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 +398,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 +443,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 +455,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 +498,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 +520,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..e8723f1f 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.
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..821d4024 100644
--- a/ext/js/data/sandbox/anki-note-data-creator.js
+++ b/ext/js/data/sandbox/anki-note-data-creator.js
@@ -28,21 +28,15 @@ export class AnkiNoteDataCreator {
* @param {JapaneseUtil} japaneseUtil An instance of `JapaneseUtil`.
*/
constructor(japaneseUtil) {
+ /** @type {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;