diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index ded343c7..2e772aa1 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -27,13 +27,12 @@ * JsonSchema * Mecab * ObjectPropertyAccessor + * OptionsUtil * TemplateRenderer * Translator * conditionsTestValue * dictTermsSort * jp - * optionsLoad - * optionsSave * profileConditionsDescriptor * profileConditionsDescriptorPromise * requestJson @@ -202,7 +201,7 @@ class Backend { this._optionsSchema = await requestJson(chrome.runtime.getURL('/bg/data/options-schema.json'), 'GET'); this._defaultAnkiFieldTemplates = (await requestText(chrome.runtime.getURL('/bg/data/default-anki-field-templates.handlebars'), 'GET')).trim(); - this._options = await optionsLoad(); + this._options = await OptionsUtil.load(); this._options = JsonSchema.getValidValueOrDefault(this._optionsSchema, this._options); this._applyOptions('background'); @@ -396,7 +395,7 @@ class Backend { async _onApiOptionsSave({source}) { const options = this.getFullOptions(); - await optionsSave(options); + await OptionsUtil.save(options); this._applyOptions(source); } diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index ccc56848..ffea96f8 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -15,383 +15,396 @@ * along with this program. If not, see . */ -/* - * Generic options functions - */ +class OptionsUtil { + static async update(options) { + // Invalid options + if (!isObject(options)) { + options = {}; + } -function optionsGetStringHashCode(string) { - let hashCode = 0; + // Check for legacy options + let defaultProfileOptions = {}; + if (!Array.isArray(options.profiles)) { + defaultProfileOptions = options; + options = {}; + } - if (typeof string !== 'string') { return hashCode; } + // Ensure profiles is an array + if (!Array.isArray(options.profiles)) { + options.profiles = []; + } - for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) { - hashCode = ((hashCode << 5) - hashCode) + charCode; - hashCode |= 0; - } - - return hashCode; -} - -function optionsGenericApplyUpdates(options, updates) { - const targetVersion = updates.length; - const currentVersion = options.version; - if (typeof currentVersion === 'number' && Number.isFinite(currentVersion)) { - for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) { - const update = updates[i]; - if (update !== null) { - update(options); + // Remove invalid profiles + const profiles = options.profiles; + for (let i = profiles.length - 1; i >= 0; --i) { + if (!isObject(profiles[i])) { + profiles.splice(i, 1); } } - } - options.version = targetVersion; - return options; -} - - -/* - * Per-profile options - */ - -const profileOptionsVersionUpdates = [ - null, - null, - null, - null, - (options) => { - options.general.audioSource = options.general.audioPlayback ? 'jpod101' : 'disabled'; - }, - (options) => { - options.general.showGuide = false; - }, - (options) => { - options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none'; - }, - (options) => { - options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split'; - options.anki.fieldTemplates = null; - }, - (options) => { - if (optionsGetStringHashCode(options.anki.fieldTemplates) === 1285806040) { - options.anki.fieldTemplates = null; - } - }, - (options) => { - if (optionsGetStringHashCode(options.anki.fieldTemplates) === -250091611) { - options.anki.fieldTemplates = null; - } - }, - (options) => { - const oldAudioSource = options.general.audioSource; - const disabled = oldAudioSource === 'disabled'; - options.audio.enabled = !disabled; - options.audio.volume = options.general.audioVolume; - options.audio.autoPlay = options.general.autoPlayAudio; - options.audio.sources = [disabled ? 'jpod101' : oldAudioSource]; - - delete options.general.audioSource; - delete options.general.audioVolume; - delete options.general.autoPlayAudio; - }, - (options) => { - // Version 12 changes: - // The preferred default value of options.anki.fieldTemplates has been changed to null. - if (optionsGetStringHashCode(options.anki.fieldTemplates) === 1444379824) { - options.anki.fieldTemplates = null; - } - }, - (options) => { - // Version 13 changes: - // Default anki field tempaltes updated to include {document-title}. - let fieldTemplates = options.anki.fieldTemplates; - if (typeof fieldTemplates === 'string') { - fieldTemplates += '\n\n{{#*inline "document-title"}}\n {{~context.document.title~}}\n{{/inline}}'; - options.anki.fieldTemplates = fieldTemplates; - } - }, - (options) => { - // Version 14 changes: - // Changed template for Anki audio and tags. - let fieldTemplates = options.anki.fieldTemplates; - if (typeof fieldTemplates !== 'string') { return; } - - const replacements = [ - [ - '{{#*inline "audio"}}{{/inline}}', - '{{#*inline "audio"}}\n {{~#if definition.audioFileName~}}\n [sound:{{definition.audioFileName}}]\n {{~/if~}}\n{{/inline}}' - ], - [ - '{{#*inline "tags"}}\n {{~#each definition.definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}}\n{{/inline}}', - '{{#*inline "tags"}}\n {{~#mergeTags definition group merge}}{{this}}{{/mergeTags~}}\n{{/inline}}' - ] - ]; - - for (const [pattern, replacement] of replacements) { - let replaced = false; - fieldTemplates = fieldTemplates.replace(new RegExp(escapeRegExp(pattern), 'g'), () => { - replaced = true; - return replacement; + // Require at least one profile + if (profiles.length === 0) { + profiles.push({ + name: 'Default', + options: defaultProfileOptions, + conditionGroups: [] }); - - if (!replaced) { - fieldTemplates += '\n\n' + replacement; - } } - options.anki.fieldTemplates = fieldTemplates; + // Ensure profileCurrent is valid + const profileCurrent = options.profileCurrent; + if (!( + typeof profileCurrent === 'number' && + Number.isFinite(profileCurrent) && + Math.floor(profileCurrent) === profileCurrent && + profileCurrent >= 0 && + profileCurrent < profiles.length + )) { + options.profileCurrent = 0; + } + + // Version + if (typeof options.version !== 'number') { + options.version = 0; + } + + // Generic updates + return await this._applyUpdates(options, this._getVersionUpdates()); } -]; -function profileOptionsCreateDefaults() { - return { - general: { - enable: true, - enableClipboardPopups: false, - resultOutputMode: 'group', - debugInfo: false, - maxResults: 32, - showAdvanced: false, - popupDisplayMode: 'default', - popupWidth: 400, - popupHeight: 250, - popupHorizontalOffset: 0, - popupVerticalOffset: 10, - popupHorizontalOffset2: 10, - popupVerticalOffset2: 0, - popupHorizontalTextPosition: 'below', - popupVerticalTextPosition: 'before', - popupScalingFactor: 1, - popupScaleRelativeToPageZoom: false, - popupScaleRelativeToVisualViewport: true, - showGuide: true, - compactTags: false, - compactGlossaries: false, - mainDictionary: '', - popupTheme: 'default', - popupOuterTheme: 'default', - customPopupCss: '', - customPopupOuterCss: '', - enableWanakana: true, - enableClipboardMonitor: false, - showPitchAccentDownstepNotation: true, - showPitchAccentPositionNotation: true, - showPitchAccentGraph: false, - showIframePopupsInRootFrame: false, - useSecurePopupFrameUrl: true, - usePopupShadowDom: true - }, - - audio: { - enabled: true, - sources: ['jpod101'], - volume: 100, - autoPlay: false, - customSourceUrl: '', - textToSpeechVoice: '' - }, - - scanning: { - middleMouse: true, - touchInputEnabled: true, - selectText: true, - alphanumeric: true, - autoHideResults: false, - delay: 20, - length: 10, - modifier: 'shift', - deepDomScan: false, - popupNestingMaxDepth: 0, - enablePopupSearch: false, - enableOnPopupExpressions: false, - enableOnSearchPage: true, - enableSearchTags: false, - layoutAwareScan: false - }, - - translation: { - convertHalfWidthCharacters: 'false', - convertNumericCharacters: 'false', - convertAlphabeticCharacters: 'false', - convertHiraganaToKatakana: 'false', - convertKatakanaToHiragana: 'variant', - collapseEmphaticSequences: 'false' - }, - - dictionaries: {}, - - parsing: { - enableScanningParser: true, - enableMecabParser: false, - selectedParser: null, - termSpacing: true, - readingMode: 'hiragana' - }, - - anki: { - enable: false, - server: 'http://127.0.0.1:8765', - tags: ['yomichan'], - sentenceExt: 200, - screenshot: {format: 'png', quality: 92}, - terms: {deck: '', model: '', fields: {}}, - kanji: {deck: '', model: '', fields: {}}, - duplicateScope: 'collection', - fieldTemplates: null + static async load() { + let options = null; + try { + const optionsStr = await new Promise((resolve, reject) => { + chrome.storage.local.get(['options'], (store) => { + const error = chrome.runtime.lastError; + if (error) { + reject(new Error(error)); + } else { + resolve(store.options); + } + }); + }); + options = JSON.parse(optionsStr); + } catch (e) { + // NOP } - }; -} -function profileOptionsSetDefaults(options) { - const defaults = profileOptionsCreateDefaults(); + return await this.update(options); + } - const combine = (target, source) => { - for (const key in source) { - if (!hasOwn(target, key)) { - target[key] = source[key]; + static save(options) { + return new Promise((resolve, reject) => { + chrome.storage.local.set({options: JSON.stringify(options)}, () => { + const error = chrome.runtime.lastError; + if (error) { + reject(new Error(error)); + } else { + resolve(); + } + }); + }); + } + + static async getDefault() { + return await this.update({}); + } + + // Legacy profile updating + + static _legacyProfileUpdateGetUpdates() { + return [ + null, + null, + null, + null, + (options) => { + options.general.audioSource = options.general.audioPlayback ? 'jpod101' : 'disabled'; + }, + (options) => { + options.general.showGuide = false; + }, + (options) => { + options.scanning.modifier = options.scanning.requireShift ? 'shift' : 'none'; + }, + (options) => { + options.general.resultOutputMode = options.general.groupResults ? 'group' : 'split'; + options.anki.fieldTemplates = null; + }, + (options) => { + if (this._getStringHashCode(options.anki.fieldTemplates) === 1285806040) { + options.anki.fieldTemplates = null; + } + }, + (options) => { + if (this._getStringHashCode(options.anki.fieldTemplates) === -250091611) { + options.anki.fieldTemplates = null; + } + }, + (options) => { + const oldAudioSource = options.general.audioSource; + const disabled = oldAudioSource === 'disabled'; + options.audio.enabled = !disabled; + options.audio.volume = options.general.audioVolume; + options.audio.autoPlay = options.general.autoPlayAudio; + options.audio.sources = [disabled ? 'jpod101' : oldAudioSource]; + + delete options.general.audioSource; + delete options.general.audioVolume; + delete options.general.autoPlayAudio; + }, + (options) => { + // Version 12 changes: + // The preferred default value of options.anki.fieldTemplates has been changed to null. + if (this._getStringHashCode(options.anki.fieldTemplates) === 1444379824) { + options.anki.fieldTemplates = null; + } + }, + (options) => { + // Version 13 changes: + // Default anki field tempaltes updated to include {document-title}. + let fieldTemplates = options.anki.fieldTemplates; + if (typeof fieldTemplates === 'string') { + fieldTemplates += '\n\n{{#*inline "document-title"}}\n {{~context.document.title~}}\n{{/inline}}'; + options.anki.fieldTemplates = fieldTemplates; + } + }, + (options) => { + // Version 14 changes: + // Changed template for Anki audio and tags. + let fieldTemplates = options.anki.fieldTemplates; + if (typeof fieldTemplates !== 'string') { return; } + + const replacements = [ + [ + '{{#*inline "audio"}}{{/inline}}', + '{{#*inline "audio"}}\n {{~#if definition.audioFileName~}}\n [sound:{{definition.audioFileName}}]\n {{~/if~}}\n{{/inline}}' + ], + [ + '{{#*inline "tags"}}\n {{~#each definition.definitionTags}}{{name}}{{#unless @last}}, {{/unless}}{{/each~}}\n{{/inline}}', + '{{#*inline "tags"}}\n {{~#mergeTags definition group merge}}{{this}}{{/mergeTags~}}\n{{/inline}}' + ] + ]; + + for (const [pattern, replacement] of replacements) { + let replaced = false; + fieldTemplates = fieldTemplates.replace(new RegExp(escapeRegExp(pattern), 'g'), () => { + replaced = true; + return replacement; + }); + + if (!replaced) { + fieldTemplates += '\n\n' + replacement; + } + } + + options.anki.fieldTemplates = fieldTemplates; } - } - }; + ]; + } - combine(options, defaults); - combine(options.general, defaults.general); - combine(options.scanning, defaults.scanning); - combine(options.anki, defaults.anki); - combine(options.anki.terms, defaults.anki.terms); - combine(options.anki.kanji, defaults.anki.kanji); + static _legacyProfileUpdateGetDefaults() { + return { + general: { + enable: true, + enableClipboardPopups: false, + resultOutputMode: 'group', + debugInfo: false, + maxResults: 32, + showAdvanced: false, + popupDisplayMode: 'default', + popupWidth: 400, + popupHeight: 250, + popupHorizontalOffset: 0, + popupVerticalOffset: 10, + popupHorizontalOffset2: 10, + popupVerticalOffset2: 0, + popupHorizontalTextPosition: 'below', + popupVerticalTextPosition: 'before', + popupScalingFactor: 1, + popupScaleRelativeToPageZoom: false, + popupScaleRelativeToVisualViewport: true, + showGuide: true, + compactTags: false, + compactGlossaries: false, + mainDictionary: '', + popupTheme: 'default', + popupOuterTheme: 'default', + customPopupCss: '', + customPopupOuterCss: '', + enableWanakana: true, + enableClipboardMonitor: false, + showPitchAccentDownstepNotation: true, + showPitchAccentPositionNotation: true, + showPitchAccentGraph: false, + showIframePopupsInRootFrame: false, + useSecurePopupFrameUrl: true, + usePopupShadowDom: true + }, - return options; -} + audio: { + enabled: true, + sources: ['jpod101'], + volume: 100, + autoPlay: false, + customSourceUrl: '', + textToSpeechVoice: '' + }, -function profileOptionsUpdateVersion(options) { - profileOptionsSetDefaults(options); - return optionsGenericApplyUpdates(options, profileOptionsVersionUpdates); -} + scanning: { + middleMouse: true, + touchInputEnabled: true, + selectText: true, + alphanumeric: true, + autoHideResults: false, + delay: 20, + length: 10, + modifier: 'shift', + deepDomScan: false, + popupNestingMaxDepth: 0, + enablePopupSearch: false, + enableOnPopupExpressions: false, + enableOnSearchPage: true, + enableSearchTags: false, + layoutAwareScan: false + }, + translation: { + convertHalfWidthCharacters: 'false', + convertNumericCharacters: 'false', + convertAlphabeticCharacters: 'false', + convertHiraganaToKatakana: 'false', + convertKatakanaToHiragana: 'variant', + collapseEmphaticSequences: 'false' + }, -/* - * Global options - * - * Each profile has an array named "conditionGroups", which is an array of condition groups - * which enable the contextual selection of profiles. The structure of the array is as follows: - * [ - * { - * conditions: [ - * { - * type: "string", - * operator: "string", - * value: "string" - * }, - * // ... - * ] - * }, - * // ... - * ] - */ + dictionaries: {}, -const optionsVersionUpdates = [ - (options) => { - options.global = { - database: { - prefixWildcardsSupported: false + parsing: { + enableScanningParser: true, + enableMecabParser: false, + selectedParser: null, + termSpacing: true, + readingMode: 'hiragana' + }, + + anki: { + enable: false, + server: 'http://127.0.0.1:8765', + tags: ['yomichan'], + sentenceExt: 200, + screenshot: {format: 'png', quality: 92}, + terms: {deck: '', model: '', fields: {}}, + kanji: {deck: '', model: '', fields: {}}, + duplicateScope: 'collection', + fieldTemplates: null } }; } -]; -function optionsUpdateVersion(options, defaultProfileOptions) { - // Ensure profiles is an array - if (!Array.isArray(options.profiles)) { - options.profiles = []; - } + static _legacyProfileUpdateAssignDefaults(options) { + const defaults = this._legacyProfileUpdateGetDefaults(); - // Remove invalid - const profiles = options.profiles; - for (let i = profiles.length - 1; i >= 0; --i) { - if (!isObject(profiles[i])) { - profiles.splice(i, 1); - } - } - - // Require at least one profile - if (profiles.length === 0) { - profiles.push({ - name: 'Default', - options: defaultProfileOptions, - conditionGroups: [] - }); - } - - // Ensure profileCurrent is valid - const profileCurrent = options.profileCurrent; - if (!( - typeof profileCurrent === 'number' && - Number.isFinite(profileCurrent) && - Math.floor(profileCurrent) === profileCurrent && - profileCurrent >= 0 && - profileCurrent < profiles.length - )) { - options.profileCurrent = 0; - } - - // Update profile options - for (const profile of profiles) { - if (!Array.isArray(profile.conditionGroups)) { - profile.conditionGroups = []; - } - profile.options = profileOptionsUpdateVersion(profile.options); - } - - // Version - if (typeof options.version !== 'number') { - options.version = 0; - } - - // Generic updates - return optionsGenericApplyUpdates(options, optionsVersionUpdates); -} - -function optionsLoad() { - return new Promise((resolve, reject) => { - chrome.storage.local.get(['options'], (store) => { - const error = chrome.runtime.lastError; - if (error) { - reject(new Error(error)); - } else { - resolve(store.options); + const combine = (target, source) => { + for (const key in source) { + if (!hasOwn(target, key)) { + target[key] = source[key]; + } } - }); - }).then((optionsStr) => { - if (typeof optionsStr === 'string') { - const options = JSON.parse(optionsStr); - if (isObject(options)) { - return options; + }; + + combine(options, defaults); + combine(options.general, defaults.general); + combine(options.scanning, defaults.scanning); + combine(options.anki, defaults.anki); + combine(options.anki.terms, defaults.anki.terms); + combine(options.anki.kanji, defaults.anki.kanji); + + return options; + } + + static _legacyProfileUpdateUpdateVersion(options) { + const updates = this._legacyProfileUpdateGetUpdates(); + this._legacyProfileUpdateAssignDefaults(options); + + const targetVersion = updates.length; + const currentVersion = options.version; + + if (typeof currentVersion === 'number' && Number.isFinite(currentVersion)) { + for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) { + const update = updates[i]; + if (update !== null) { + update(options); + } } } - return {}; - }).catch(() => { - return {}; - }).then((options) => { - return ( - Array.isArray(options.profiles) ? - optionsUpdateVersion(options, {}) : - optionsUpdateVersion({}, options) - ); - }); -} -function optionsSave(options) { - return new Promise((resolve, reject) => { - chrome.storage.local.set({options: JSON.stringify(options)}, () => { - const error = chrome.runtime.lastError; - if (error) { - reject(new Error(error)); - } else { - resolve(); + options.version = targetVersion; + return options; + } + + // Private + + static _getStringHashCode(string) { + let hashCode = 0; + + if (typeof string !== 'string') { return hashCode; } + + for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) { + hashCode = ((hashCode << 5) - hashCode) + charCode; + hashCode |= 0; + } + + return hashCode; + } + + static async _applyUpdates(options, updates) { + const targetVersion = updates.length; + let currentVersion = options.version; + + if (typeof currentVersion !== 'number' || !Number.isFinite(currentVersion)) { + currentVersion = 0; + } + + for (let i = Math.max(0, Math.floor(currentVersion)); i < targetVersion; ++i) { + const {update, async} = updates[i]; + const result = update(options); + options = (async ? await result : result); + } + + options.version = targetVersion; + return options; + } + + static _getVersionUpdates() { + return [ + { + async: false, + update: (options) => { + // Version 1 changes: + // Added options.global.database.prefixWildcardsSupported = false + options.global = { + database: { + prefixWildcardsSupported: false + } + }; + return options; + } + }, + { + async: false, + update: (options) => { + // Version 2 changes: + // Legacy profile update process moved into this upgrade function. + for (const profile of options.profiles) { + if (!Array.isArray(profile.conditionGroups)) { + profile.conditionGroups = []; + } + profile.options = this._legacyProfileUpdateUpdateVersion(profile.options); + } + return options; + } } - }); - }); -} - -function optionsGetDefault() { - return optionsUpdateVersion({}, {}); + ]; + } } diff --git a/ext/bg/js/settings/backup.js b/ext/bg/js/settings/backup.js index 13f90886..57963cec 100644 --- a/ext/bg/js/settings/backup.js +++ b/ext/bg/js/settings/backup.js @@ -16,9 +16,8 @@ */ /* global + * OptionsUtil * api - * optionsGetDefault - * optionsUpdateVersion */ class SettingsBackup { @@ -323,7 +322,7 @@ class SettingsBackup { } // Upgrade options - optionsFull = optionsUpdateVersion(optionsFull, {}); + optionsFull = await OptionsUtil.update(optionsFull); // Check for warnings const sanitizationWarnings = this._settingsImportSanitizeOptions(optionsFull, true); @@ -369,7 +368,7 @@ class SettingsBackup { $('#settings-reset-modal').modal('hide'); // Get default options - const optionsFull = optionsGetDefault(); + const optionsFull = await OptionsUtil.getDefault(); // Assign options await this._settingsImportSetOptionsFull(optionsFull);