diff options
Diffstat (limited to 'ext/bg/js/options.js')
-rw-r--r-- | ext/bg/js/options.js | 739 |
1 files changed, 0 insertions, 739 deletions
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js deleted file mode 100644 index 1105dfed..00000000 --- a/ext/bg/js/options.js +++ /dev/null @@ -1,739 +0,0 @@ -/* - * Copyright (C) 2016-2021 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. - */ - -/* global - * JsonSchemaValidator - * TemplatePatcher - */ - -class OptionsUtil { - constructor() { - this._schemaValidator = new JsonSchemaValidator(); - this._templatePatcher = null; - this._optionsSchema = null; - } - - async prepare() { - this._optionsSchema = await this._fetchAsset('/data/schemas/options-schema.json', true); - } - - async update(options) { - // Invalid options - if (!isObject(options)) { - options = {}; - } - - // Check for legacy options - let defaultProfileOptions = {}; - if (!Array.isArray(options.profiles)) { - defaultProfileOptions = options; - options = {}; - } - - // Ensure profiles is an array - if (!Array.isArray(options.profiles)) { - options.profiles = []; - } - - // Remove invalid profiles - 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; - } - - // Version - if (typeof options.version !== 'number') { - options.version = 0; - } - - // Generic updates - options = await this._applyUpdates(options, this._getVersionUpdates()); - - // Validation - options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema, options); - - // Result - return options; - } - - async load() { - let options; - 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.message)); - } else { - resolve(store.options); - } - }); - }); - options = JSON.parse(optionsStr); - } catch (e) { - // NOP - } - - if (typeof options !== 'undefined') { - options = await this.update(options); - } else { - options = this.getDefault(); - } - - return options; - } - - 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.message)); - } else { - resolve(); - } - }); - }); - } - - getDefault() { - const optionsVersion = this._getVersionUpdates().length; - const options = this._schemaValidator.getValidValueOrDefault(this._optionsSchema); - options.version = optionsVersion; - return options; - } - - createValidatingProxy(options) { - return this._schemaValidator.createProxy(options, this._optionsSchema); - } - - validate(options) { - return this._schemaValidator.validate(options, this._optionsSchema); - } - - // Legacy profile updating - - _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; - } - ]; - } - - _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 - } - }; - } - - _legacyProfileUpdateAssignDefaults(options) { - const defaults = this._legacyProfileUpdateGetDefaults(); - - const combine = (target, source) => { - for (const key in source) { - if (!Object.prototype.hasOwnProperty.call(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); - - return options; - } - - _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); - } - } - } - - options.version = targetVersion; - return options; - } - - // Private - - async _applyAnkiFieldTemplatesPatch(options, modificationsUrl) { - let patch = null; - for (const {options: profileOptions} of options.profiles) { - const fieldTemplates = profileOptions.anki.fieldTemplates; - if (fieldTemplates === null) { continue; } - - if (patch === null) { - const content = await this._fetchAsset(modificationsUrl); - if (this._templatePatcher === null) { - this._templatePatcher = new TemplatePatcher(); - } - patch = this._templatePatcher.parsePatch(content); - } - - profileOptions.anki.fieldTemplates = this._templatePatcher.applyPatch(fieldTemplates, patch); - } - } - - async _fetchAsset(url, json=false) { - url = chrome.runtime.getURL(url); - const response = await fetch(url, { - method: 'GET', - mode: 'no-cors', - cache: 'default', - credentials: 'omit', - redirect: 'follow', - referrerPolicy: 'no-referrer' - }); - if (!response.ok) { - throw new Error(`Failed to fetch ${url}: ${response.status}`); - } - return await (json ? response.json() : response.text()); - } - - _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; - } - - 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; - } - - _getVersionUpdates() { - return [ - {async: false, update: this._updateVersion1.bind(this)}, - {async: false, update: this._updateVersion2.bind(this)}, - {async: true, update: this._updateVersion3.bind(this)}, - {async: true, update: this._updateVersion4.bind(this)}, - {async: false, update: this._updateVersion5.bind(this)}, - {async: true, update: this._updateVersion6.bind(this)}, - {async: false, update: this._updateVersion7.bind(this)}, - {async: true, update: this._updateVersion8.bind(this)} - ]; - } - - _updateVersion1(options) { - // Version 1 changes: - // Added options.global.database.prefixWildcardsSupported = false. - options.global = { - database: { - prefixWildcardsSupported: false - } - }; - return options; - } - - _updateVersion2(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; - } - - async _updateVersion3(options) { - // Version 3 changes: - // Pitch accent Anki field templates added. - await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v2.handlebars'); - return options; - } - - async _updateVersion4(options) { - // Version 4 changes: - // Options conditions converted to string representations. - // Added usePopupWindow. - // Updated handlebars templates to include "clipboard-image" definition. - // Updated handlebars templates to include "clipboard-text" definition. - // Added hideDelay. - // Added inputs to profileOptions.scanning. - // Added pointerEventsEnabled to profileOptions.scanning. - // Added preventMiddleMouse to profileOptions.scanning. - for (const {conditionGroups} of options.profiles) { - for (const {conditions} of conditionGroups) { - for (const condition of conditions) { - const value = condition.value; - condition.value = ( - Array.isArray(value) ? - value.join(', ') : - `${value}` - ); - } - } - } - const createInputDefaultOptions = () => ({ - showAdvanced: false, - searchTerms: true, - searchKanji: true, - scanOnTouchMove: true, - scanOnPenHover: true, - scanOnPenPress: true, - scanOnPenRelease: false, - preventTouchScrolling: true - }); - for (const {options: profileOptions} of options.profiles) { - profileOptions.general.usePopupWindow = false; - profileOptions.scanning.hideDelay = 0; - profileOptions.scanning.pointerEventsEnabled = false; - profileOptions.scanning.preventMiddleMouse = { - onWebPages: false, - onPopupPages: false, - onSearchPages: false, - onSearchQuery: false - }; - - const {modifier, middleMouse} = profileOptions.scanning; - delete profileOptions.scanning.modifier; - delete profileOptions.scanning.middleMouse; - const scanningInputs = []; - let modifierInput = ''; - switch (modifier) { - case 'alt': - case 'ctrl': - case 'shift': - case 'meta': - modifierInput = modifier; - break; - case 'none': - modifierInput = ''; - break; - } - scanningInputs.push({ - include: modifierInput, - exclude: 'mouse0', - types: {mouse: true, touch: false, pen: false}, - options: createInputDefaultOptions() - }); - if (middleMouse) { - scanningInputs.push({ - include: 'mouse2', - exclude: '', - types: {mouse: true, touch: false, pen: false}, - options: createInputDefaultOptions() - }); - } - scanningInputs.push({ - include: '', - exclude: '', - types: {mouse: false, touch: true, pen: true}, - options: createInputDefaultOptions() - }); - profileOptions.scanning.inputs = scanningInputs; - } - await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v4.handlebars'); - return options; - } - - _updateVersion5(options) { - // Version 5 changes: - // Removed legacy version number from profile options. - for (const profile of options.profiles) { - delete profile.options.version; - } - return options; - } - - async _updateVersion6(options) { - // Version 6 changes: - // Updated handlebars templates to include "conjugation" definition. - // Added global option showPopupPreview. - // Added global option useSettingsV2. - // Added anki.checkForDuplicates. - // Added general.glossaryLayoutMode; removed general.compactGlossaries. - await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v6.handlebars'); - options.global.showPopupPreview = false; - options.global.useSettingsV2 = false; - for (const profile of options.profiles) { - profile.options.anki.checkForDuplicates = true; - profile.options.general.glossaryLayoutMode = (profile.options.general.compactGlossaries ? 'compact' : 'default'); - delete profile.options.general.compactGlossaries; - const fieldTemplates = profile.options.anki.fieldTemplates; - if (typeof fieldTemplates === 'string') { - profile.options.anki.fieldTemplates = this._updateVersion6AnkiTemplatesCompactTags(fieldTemplates); - } - } - return options; - } - - _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'); - const replacement1 = ( - // eslint-disable-next-line indent -`{{~#scope~}} - {{~#set "any" false}}{{/set~}} - {{~#if definitionTags~}}{{#each definitionTags~}} - {{~#if (op "||" (op "!" ../data.compactTags) (op "!" redundant))~}} - {{~#if (get "any")}}, {{else}}<i>({{/if~}} - {{name}} - {{~#set "any" true}}{{/set~}} - {{~/if~}} - {{~/each~}} - {{~#if (get "any")}})</i> {{/if~}} - {{~/if~}} -{{~/scope~}}` - ); - const simpleNewline = /\n/g; - templates = templates.replace(pattern1, (g0, space) => (space + replacement1.replace(simpleNewline, space))); - templates = templates.replace(/\bcompactGlossaries=((?:\.*\/)*)compactGlossaries\b/g, (g0, g1) => `${g0} data=${g1}.`); - return templates; - } - - _updateVersion7(options) { - // Version 7 changes: - // Added general.maximumClipboardSearchLength. - // Added general.popupCurrentIndicatorMode. - // Added general.popupActionBarVisibility. - // Added general.popupActionBarLocation. - // Removed global option showPopupPreview. - delete options.global.showPopupPreview; - for (const profile of options.profiles) { - profile.options.general.maximumClipboardSearchLength = 1000; - profile.options.general.popupCurrentIndicatorMode = 'triangle'; - profile.options.general.popupActionBarVisibility = 'auto'; - profile.options.general.popupActionBarLocation = 'right'; - } - return options; - } - - async _updateVersion8(options) { - // Version 8 changes: - // Added translation.textReplacements. - // Moved anki.sentenceExt to sentenceParsing.scanExtent. - // Added sentenceParsing.enableTerminationCharacters. - // Added sentenceParsing.terminationCharacters. - // Changed general.popupActionBarLocation. - // Added inputs.hotkeys. - // Added anki.suspendNewCards. - // Added popupWindow. - // Updated handlebars templates to include "stroke-count" definition. - // Updated global.useSettingsV2 to be true (opt-out). - // Added audio.customSourceType. - // Moved general.enableClipboardPopups => clipboard.enableBackgroundMonitor. - // Moved general.enableClipboardMonitor => clipboard.enableSearchPageMonitor. Forced value to false due to a bug which caused its value to not be read. - // Moved general.maximumClipboardSearchLength => clipboard.maximumSearchLength. - // Added clipboard.autoSearchContent. - await this._applyAnkiFieldTemplatesPatch(options, '/data/templates/anki-field-templates-upgrade-v8.handlebars'); - options.global.useSettingsV2 = true; - for (const profile of options.profiles) { - profile.options.translation.textReplacements = { - searchOriginal: true, - groups: [] - }; - profile.options.sentenceParsing = { - scanExtent: profile.options.anki.sentenceExt, - enableTerminationCharacters: true, - terminationCharacters: [ - {enabled: true, character1: '「', character2: '」', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '『', character2: '』', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '"', character2: '"', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '\'', character2: '\'', includeCharacterAtStart: false, includeCharacterAtEnd: false}, - {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '.', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '。', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '!', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '?', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true}, - {enabled: true, character1: '…', character2: null, includeCharacterAtStart: false, includeCharacterAtEnd: true} - ] - }; - delete profile.options.anki.sentenceExt; - profile.options.general.popupActionBarLocation = 'top'; - profile.options.inputs = { - hotkeys: [ - {action: 'close', key: 'Escape', modifiers: [], scopes: ['popup'], enabled: true}, - {action: 'focusSearchBox', key: 'Escape', modifiers: [], scopes: ['search'], enabled: true}, - {action: 'previousEntry3', key: 'PageUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'nextEntry3', key: 'PageDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'lastEntry', key: 'End', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'firstEntry', key: 'Home', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'previousEntry', key: 'ArrowUp', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'nextEntry', key: 'ArrowDown', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'historyBackward', key: 'KeyB', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'historyForward', key: 'KeyF', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteKanji', key: 'KeyK', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteTermKanji', key: 'KeyE', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'addNoteTermKana', key: 'KeyR', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'playAudio', key: 'KeyP', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'viewNote', key: 'KeyV', modifiers: ['alt'], scopes: ['popup', 'search'], enabled: true}, - {action: 'copyHostSelection', key: 'KeyC', modifiers: ['ctrl'], scopes: ['popup'], enabled: true} - ] - }; - profile.options.anki.suspendNewCards = false; - profile.options.popupWindow = { - width: profile.options.general.popupWidth, - height: profile.options.general.popupHeight, - left: 0, - top: 0, - useLeft: false, - useTop: false, - windowType: 'popup', - windowState: 'normal' - }; - profile.options.audio.customSourceType = 'audio'; - profile.options.clipboard = { - enableBackgroundMonitor: profile.options.general.enableClipboardPopups, - enableSearchPageMonitor: false, - autoSearchContent: true, - maximumSearchLength: profile.options.general.maximumClipboardSearchLength - }; - delete profile.options.general.enableClipboardPopups; - delete profile.options.general.enableClipboardMonitor; - delete profile.options.general.maximumClipboardSearchLength; - } - return options; - } -} |