summaryrefslogtreecommitdiff
path: root/ext/bg/js/options.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/bg/js/options.js')
-rw-r--r--ext/bg/js/options.js693
1 files changed, 353 insertions, 340 deletions
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;
+ }
+ }
+ ];
+ }
}