summaryrefslogtreecommitdiff
path: root/ext/js/data/options-util.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/js/data/options-util.js')
-rw-r--r--ext/js/data/options-util.js192
1 files changed, 172 insertions, 20 deletions
diff --git a/ext/js/data/options-util.js b/ext/js/data/options-util.js
index 3858cb55..70c1622f 100644
--- a/ext/js/data/options-util.js
+++ b/ext/js/data/options-util.js
@@ -22,20 +22,29 @@ import {JsonSchema} from './json-schema.js';
export class OptionsUtil {
constructor() {
+ /** @type {?TemplatePatcher} */
this._templatePatcher = null;
+ /** @type {?JsonSchema} */
this._optionsSchema = null;
}
+ /** */
async prepare() {
- const schema = await this._fetchAsset('/data/schemas/options-schema.json', true);
+ const schema = /** @type {import('json-schema').Schema} */ (await this._fetchJson('/data/schemas/options-schema.json'));
this._optionsSchema = new JsonSchema(schema);
}
- async update(options, targetVersion=null) {
+ /**
+ * @param {unknown} optionsInput
+ * @param {?number} [targetVersion]
+ * @returns {Promise<import('settings').Options>}
+ */
+ async update(optionsInput, targetVersion=null) {
// Invalid options
- if (!isObject(options)) {
- options = {};
- }
+ let options = /** @type {{[key: string]: unknown}} */ (
+ typeof optionsInput === 'object' && optionsInput !== null && !Array.isArray(optionsInput) ?
+ optionsInput : {}
+ );
// Check for legacy options
let defaultProfileOptions = {};
@@ -50,7 +59,7 @@ export class OptionsUtil {
}
// Remove invalid profiles
- const profiles = options.profiles;
+ const profiles = /** @type {unknown[]} */ (options.profiles);
for (let i = profiles.length - 1; i >= 0; --i) {
if (!isObject(profiles[i])) {
profiles.splice(i, 1);
@@ -87,12 +96,12 @@ export class OptionsUtil {
options = await this._applyUpdates(options, this._getVersionUpdates(targetVersion));
// Validation
- options = this._optionsSchema.getValidValueOrDefault(options);
-
- // Result
- return options;
+ return /** @type {import('settings').Options} */ (/** @type {JsonSchema} */ (this._optionsSchema).getValidValueOrDefault(options));
}
+ /**
+ * @returns {Promise<import('settings').Options>}
+ */
async load() {
let options;
try {
@@ -121,6 +130,10 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @param {import('settings').Options} options
+ * @returns {Promise<void>}
+ */
save(options) {
return new Promise((resolve, reject) => {
chrome.storage.local.set({options: JSON.stringify(options)}, () => {
@@ -134,23 +147,36 @@ export class OptionsUtil {
});
}
+ /**
+ * @returns {import('settings').Options}
+ */
getDefault() {
- const optionsVersion = this._getVersionUpdates().length;
- const options = this._optionsSchema.getValidValueOrDefault();
+ const optionsVersion = this._getVersionUpdates(null).length;
+ const options = /** @type {import('settings').Options} */ (/** @type {JsonSchema} */ (this._optionsSchema).getValidValueOrDefault());
options.version = optionsVersion;
return options;
}
+ /**
+ * @param {import('settings').Options} options
+ * @returns {import('settings').Options}
+ */
createValidatingProxy(options) {
- return this._optionsSchema.createProxy(options);
+ return /** @type {import('settings').Options} */ (/** @type {JsonSchema} */ (this._optionsSchema).createProxy(options));
}
+ /**
+ * @param {import('settings').Options} options
+ */
validate(options) {
- return this._optionsSchema.validate(options);
+ /** @type {JsonSchema} */ (this._optionsSchema).validate(options);
}
// Legacy profile updating
+ /**
+ * @returns {(?import('options-util').LegacyUpdateFunction)[]}
+ */
_legacyProfileUpdateGetUpdates() {
return [
null,
@@ -242,6 +268,9 @@ export class OptionsUtil {
];
}
+ /**
+ * @returns {import('options-util').LegacyOptions}
+ */
_legacyProfileUpdateGetDefaults() {
return {
general: {
@@ -341,9 +370,17 @@ export class OptionsUtil {
};
}
+ /**
+ * @param {import('options-util').IntermediateOptions} options
+ * @returns {import('options-util').IntermediateOptions}
+ */
_legacyProfileUpdateAssignDefaults(options) {
const defaults = this._legacyProfileUpdateGetDefaults();
+ /**
+ * @param {import('options-util').IntermediateOptions} target
+ * @param {import('core').UnknownObject} source
+ */
const combine = (target, source) => {
for (const key in source) {
if (!Object.prototype.hasOwnProperty.call(target, key)) {
@@ -362,6 +399,10 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @param {import('options-util').IntermediateOptions} options
+ * @returns {import('options-util').IntermediateOptions}
+ */
_legacyProfileUpdateUpdateVersion(options) {
const updates = this._legacyProfileUpdateGetUpdates();
this._legacyProfileUpdateAssignDefaults(options);
@@ -384,6 +425,10 @@ export class OptionsUtil {
// Private
+ /**
+ * @param {import('options-util').IntermediateOptions} options
+ * @param {string} modificationsUrl
+ */
async _applyAnkiFieldTemplatesPatch(options, modificationsUrl) {
let patch = null;
for (const {options: profileOptions} of options.profiles) {
@@ -391,18 +436,22 @@ export class OptionsUtil {
if (fieldTemplates === null) { continue; }
if (patch === null) {
- const content = await this._fetchAsset(modificationsUrl);
+ const content = await this._fetchText(modificationsUrl);
if (this._templatePatcher === null) {
this._templatePatcher = new TemplatePatcher();
}
patch = this._templatePatcher.parsePatch(content);
}
- profileOptions.anki.fieldTemplates = this._templatePatcher.applyPatch(fieldTemplates, patch);
+ profileOptions.anki.fieldTemplates = /** @type {TemplatePatcher} */ (this._templatePatcher).applyPatch(fieldTemplates, patch);
}
}
- async _fetchAsset(url, json=false) {
+ /**
+ * @param {string} url
+ * @returns {Promise<Response>}
+ */
+ async _fetchGeneric(url) {
url = chrome.runtime.getURL(url);
const response = await fetch(url, {
method: 'GET',
@@ -415,9 +464,31 @@ export class OptionsUtil {
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
- return await (json ? response.json() : response.text());
+ return response;
+ }
+
+ /**
+ * @param {string} url
+ * @returns {Promise<string>}
+ */
+ async _fetchText(url) {
+ const response = await this._fetchGeneric(url);
+ return await response.text();
+ }
+
+ /**
+ * @param {string} url
+ * @returns {Promise<unknown>}
+ */
+ async _fetchJson(url) {
+ const response = await this._fetchGeneric(url);
+ return await response.json();
}
+ /**
+ * @param {string} string
+ * @returns {number}
+ */
_getStringHashCode(string) {
let hashCode = 0;
@@ -431,6 +502,11 @@ export class OptionsUtil {
return hashCode;
}
+ /**
+ * @param {import('options-util').IntermediateOptions} options
+ * @param {import('options-util').ModernUpdate[]} updates
+ * @returns {Promise<import('settings').Options>}
+ */
async _applyUpdates(options, updates) {
const targetVersion = updates.length;
let currentVersion = options.version;
@@ -449,6 +525,10 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @param {?number} targetVersion
+ * @returns {import('options-util').ModernUpdate[]}
+ */
_getVersionUpdates(targetVersion) {
const result = [
{async: false, update: this._updateVersion1.bind(this)},
@@ -479,6 +559,9 @@ export class OptionsUtil {
return result;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion1(options) {
// Version 1 changes:
// Added options.global.database.prefixWildcardsSupported = false.
@@ -490,6 +573,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion2(options) {
// Version 2 changes:
// Legacy profile update process moved into this upgrade function.
@@ -502,6 +588,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionAsync}
+ */
async _updateVersion3(options) {
// Version 3 changes:
// Pitch accent Anki field templates added.
@@ -509,6 +598,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionAsync}
+ */
async _updateVersion4(options) {
// Version 4 changes:
// Options conditions converted to string representations.
@@ -594,6 +686,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion5(options) {
// Version 5 changes:
// Removed legacy version number from profile options.
@@ -603,6 +698,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionAsync}
+ */
async _updateVersion6(options) {
// Version 6 changes:
// Updated handlebars templates to include "conjugation" definition.
@@ -625,6 +723,10 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @param {string} templates
+ * @returns {string}
+ */
_updateVersion6AnkiTemplatesCompactTags(templates) {
const rawPattern1 = '{{~#if definitionTags~}}<i>({{#each definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each}})</i> {{/if~}}';
const pattern1 = new RegExp(`((\r?\n)?[ \t]*)${escapeRegExp(rawPattern1)}`, 'g');
@@ -649,6 +751,9 @@ export class OptionsUtil {
return templates;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion7(options) {
// Version 7 changes:
// Added general.maximumClipboardSearchLength.
@@ -666,6 +771,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionAsync}
+ */
async _updateVersion8(options) {
// Version 8 changes:
// Added translation.textReplacements.
@@ -755,6 +863,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion9(options) {
// Version 9 changes:
// Added general.frequencyDisplayMode.
@@ -766,6 +877,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionAsync}
+ */
async _updateVersion10(options) {
// Version 10 changes:
// Removed global option useSettingsV2.
@@ -803,6 +917,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion11(options) {
// Version 11 changes:
// Changed dictionaries to an array.
@@ -827,6 +944,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionAsync}
+ */
async _updateVersion12(options) {
// Version 12 changes:
// Changed sentenceParsing.enableTerminationCharacters to sentenceParsing.terminationCharacterMode.
@@ -841,7 +961,7 @@ export class OptionsUtil {
delete sentenceParsing.enableTerminationCharacters;
const {sources, customSourceUrl, customSourceType, textToSpeechVoice} = audio;
- audio.sources = sources.map((type) => {
+ audio.sources = /** @type {string[]} */ (sources).map((type) => {
switch (type) {
case 'text-to-speech':
case 'text-to-speech-reading':
@@ -859,6 +979,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionAsync}
+ */
async _updateVersion13(options) {
// Version 13 changes:
// Handlebars templates updated to use formatGlossary.
@@ -874,6 +997,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion14(options) {
// Version 14 changes:
// Added accessibility options.
@@ -885,6 +1011,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion15(options) {
// Version 15 changes:
// Added general.sortFrequencyDictionary.
@@ -896,6 +1025,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion16(options) {
// Version 16 changes:
// Added scanning.matchTypePrefix.
@@ -905,12 +1037,16 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion17(options) {
// Version 17 changes:
// Added vertical sentence punctuation to terminationCharacters.
const additions = ['︒', '︕', '︖', '︙'];
for (const profile of options.profiles) {
- const {terminationCharacters} = profile.options.sentenceParsing;
+ /** @type {import('settings').SentenceParsingTerminationCharacterOption[]} */
+ const terminationCharacters = profile.options.sentenceParsing.terminationCharacters;
const newAdditions = [];
for (const character of additions) {
if (terminationCharacters.findIndex((value) => (value.character1 === character && value.character2 === null)) < 0) {
@@ -930,6 +1066,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion18(options) {
// Version 18 changes:
// general.popupTheme's 'default' value changed to 'light'
@@ -952,6 +1091,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion19(options) {
// Version 19 changes:
// Added anki.noteGuiMode.
@@ -979,6 +1121,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionSync}
+ */
_updateVersion20(options) {
// Version 20 changes:
// Added anki.downloadTimeout.
@@ -999,6 +1144,9 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @type {import('options-util').ModernUpdateFunctionAsync}
+ */
async _updateVersion21(options) {
await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v21.handlebars');
@@ -1018,6 +1166,10 @@ export class OptionsUtil {
return options;
}
+ /**
+ * @param {string} url
+ * @returns {Promise<chrome.tabs.Tab>}
+ */
_createTab(url) {
return new Promise((resolve, reject) => {
chrome.tabs.create({url}, (tab) => {