diff options
| -rw-r--r-- | ext/bg/js/backend.js | 7 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 693 | ||||
| -rw-r--r-- | ext/bg/js/settings/backup.js | 7 | 
3 files changed, 359 insertions, 348 deletions
| 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 <https://www.gnu.org/licenses/>.   */ -/* - * Generic options functions - */ - -function optionsGetStringHashCode(string) { -    let hashCode = 0; - -    if (typeof string !== 'string') { return hashCode; } +class OptionsUtil { +    static async update(options) { +        // Invalid options +        if (!isObject(options)) { +            options = {}; +        } -    for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) { -        hashCode = ((hashCode << 5) - hashCode) + charCode; -        hashCode |= 0; -    } +        // Check for legacy options +        let defaultProfileOptions = {}; +        if (!Array.isArray(options.profiles)) { +            defaultProfileOptions = options; +            options = {}; +        } -    return hashCode; -} +        // Ensure profiles is an array +        if (!Array.isArray(options.profiles)) { +            options.profiles = []; +        } -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; +        // Require at least one profile +        if (profiles.length === 0) { +            profiles.push({ +                name: 'Default', +                options: defaultProfileOptions, +                conditionGroups: [] +            });          } -    }, -    (options) => { -        if (optionsGetStringHashCode(options.anki.fieldTemplates) === -250091611) { -            options.anki.fieldTemplates = null; + +        // 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;          } -    }, -    (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; + +        // Version +        if (typeof options.version !== 'number') { +            options.version = 0;          } -    }, -    (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; + +        // Generic updates +        return await this._applyUpdates(options, this._getVersionUpdates()); +    } + +    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          } -    }, -    (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; +        return await this.update(options); +    } + +    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(); +                }              }); +        }); +    } -            if (!replaced) { -                fieldTemplates += '\n\n' + replacement; +    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;              } -        } +        ]; +    } -        options.anki.fieldTemplates = fieldTemplates; +    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 +            }, + +            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 +            } +        };      } -]; - -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 -        } -    }; -} -function profileOptionsSetDefaults(options) { -    const defaults = profileOptionsCreateDefaults(); +    static _legacyProfileUpdateAssignDefaults(options) { +        const defaults = this._legacyProfileUpdateGetDefaults(); -    const combine = (target, source) => { -        for (const key in source) { -            if (!hasOwn(target, key)) { -                target[key] = source[key]; +        const combine = (target, source) => { +            for (const key in source) { +                if (!hasOwn(target, key)) { +                    target[key] = source[key]; +                }              } -        } -    }; +        }; -    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); +        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; -} - -function profileOptionsUpdateVersion(options) { -    profileOptionsSetDefaults(options); -    return optionsGenericApplyUpdates(options, profileOptionsVersionUpdates); -} +        return options; +    } +    static _legacyProfileUpdateUpdateVersion(options) { +        const updates = this._legacyProfileUpdateGetUpdates(); +        this._legacyProfileUpdateAssignDefaults(options); -/* - * 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" - *             }, - *             // ... - *         ] - *     }, - *     // ... - * ] - */ +        const targetVersion = updates.length; +        const currentVersion = options.version; -const optionsVersionUpdates = [ -    (options) => { -        options.global = { -            database: { -                prefixWildcardsSupported: false +        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); +                }              } -        }; -    } -]; +        } -function optionsUpdateVersion(options, defaultProfileOptions) { -    // Ensure profiles is an array -    if (!Array.isArray(options.profiles)) { -        options.profiles = []; +        options.version = targetVersion; +        return options;      } -    // Remove invalid -    const profiles = options.profiles; -    for (let i = profiles.length - 1; i >= 0; --i) { -        if (!isObject(profiles[i])) { -            profiles.splice(i, 1); -        } -    } +    // Private -    // Require at least one profile -    if (profiles.length === 0) { -        profiles.push({ -            name: 'Default', -            options: defaultProfileOptions, -            conditionGroups: [] -        }); -    } +    static _getStringHashCode(string) { +        let hashCode = 0; -    // 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; -    } +        if (typeof string !== 'string') { return hashCode; } -    // Update profile options -    for (const profile of profiles) { -        if (!Array.isArray(profile.conditionGroups)) { -            profile.conditionGroups = []; +        for (let i = 0, charCode = string.charCodeAt(i); i < string.length; charCode = string.charCodeAt(++i)) { +            hashCode = ((hashCode << 5) - hashCode) + charCode; +            hashCode |= 0;          } -        profile.options = profileOptionsUpdateVersion(profile.options); -    } -    // Version -    if (typeof options.version !== 'number') { -        options.version = 0; +        return hashCode;      } -    // Generic updates -    return optionsGenericApplyUpdates(options, optionsVersionUpdates); -} +    static async _applyUpdates(options, updates) { +        const targetVersion = updates.length; +        let currentVersion = options.version; -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); -            } -        }); -    }).then((optionsStr) => { -        if (typeof optionsStr === 'string') { -            const options = JSON.parse(optionsStr); -            if (isObject(options)) { -                return options; -            } +        if (typeof currentVersion !== 'number' || !Number.isFinite(currentVersion)) { +            currentVersion = 0;          } -        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(); -            } -        }); -    }); -} +        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; +    } -function optionsGetDefault() { -    return optionsUpdateVersion({}, {}); +    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; +                } +            } +        ]; +    }  } 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); |