2017-07-16 20:14:28 +00:00
/ *
2022-02-03 01:43:10 +00:00
* Copyright ( C ) 2016 - 2022 Yomichan Authors
2017-07-16 20:14:28 +00:00
*
* 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
2020-01-01 17:00:31 +00:00
* along with this program . If not , see < https : //www.gnu.org/licenses/>.
2017-07-16 20:14:28 +00:00
* /
2020-09-11 18:15:08 +00:00
/ * g l o b a l
2021-05-22 19:45:20 +00:00
* JsonSchema
2021-01-29 02:17:10 +00:00
* TemplatePatcher
2020-09-11 18:15:08 +00:00
* /
2020-08-01 15:46:35 +00:00
class OptionsUtil {
2020-09-11 18:15:08 +00:00
constructor ( ) {
2021-01-29 02:17:10 +00:00
this . _templatePatcher = null ;
2020-09-11 18:15:08 +00:00
this . _optionsSchema = null ;
}
async prepare ( ) {
2021-05-22 19:45:20 +00:00
const schema = await this . _fetchAsset ( '/data/schemas/options-schema.json' , true ) ;
this . _optionsSchema = new JsonSchema ( schema ) ;
2020-09-11 18:15:08 +00:00
}
2021-07-06 03:24:06 +00:00
async update ( options , targetVersion = null ) {
2020-08-01 15:46:35 +00:00
// Invalid options
if ( ! isObject ( options ) ) {
options = { } ;
}
2020-04-11 19:23:32 +00:00
2020-08-01 15:46:35 +00:00
// Check for legacy options
let defaultProfileOptions = { } ;
if ( ! Array . isArray ( options . profiles ) ) {
defaultProfileOptions = options ;
options = { } ;
}
2020-04-11 19:23:32 +00:00
2020-08-01 15:46:35 +00:00
// Ensure profiles is an array
if ( ! Array . isArray ( options . profiles ) ) {
options . profiles = [ ] ;
}
2020-04-11 19:23:32 +00:00
2020-08-01 15:46:35 +00:00
// Remove invalid profiles
const profiles = options . profiles ;
for ( let i = profiles . length - 1 ; i >= 0 ; -- i ) {
if ( ! isObject ( profiles [ i ] ) ) {
profiles . splice ( i , 1 ) ;
2019-09-06 22:21:20 +00:00
}
}
2019-09-07 23:50:58 +00:00
2020-08-01 15:46:35 +00:00
// Require at least one profile
if ( profiles . length === 0 ) {
profiles . push ( {
name : 'Default' ,
options : defaultProfileOptions ,
conditionGroups : [ ]
} ) ;
2019-09-06 22:21:20 +00:00
}
2020-08-01 15:46:35 +00:00
// 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 ;
2019-09-06 22:21:20 +00:00
}
2020-08-01 15:46:35 +00:00
// Version
if ( typeof options . version !== 'number' ) {
options . version = 0 ;
2019-12-15 05:07:54 +00:00
}
2020-08-01 15:46:35 +00:00
// Generic updates
2021-07-06 03:24:06 +00:00
options = await this . _applyUpdates ( options , this . _getVersionUpdates ( targetVersion ) ) ;
2020-10-27 23:26:30 +00:00
// Validation
2021-05-22 19:45:20 +00:00
options = this . _optionsSchema . getValidValueOrDefault ( options ) ;
2020-10-27 23:26:30 +00:00
// Result
return options ;
2020-08-01 15:46:35 +00:00
}
2020-09-11 18:15:08 +00:00
async load ( ) {
let options ;
2020-08-01 15:46:35 +00:00
try {
const optionsStr = await new Promise ( ( resolve , reject ) => {
chrome . storage . local . get ( [ 'options' ] , ( store ) => {
const error = chrome . runtime . lastError ;
if ( error ) {
2020-09-11 18:15:08 +00:00
reject ( new Error ( error . message ) ) ;
2020-08-01 15:46:35 +00:00
} else {
resolve ( store . options ) ;
}
} ) ;
} ) ;
options = JSON . parse ( optionsStr ) ;
} catch ( e ) {
// NOP
2020-03-15 21:32:31 +00:00
}
2020-05-02 16:50:16 +00:00
2020-09-11 18:15:08 +00:00
if ( typeof options !== 'undefined' ) {
options = await this . update ( options ) ;
2020-10-27 23:26:30 +00:00
} else {
options = this . getDefault ( ) ;
2020-09-11 18:15:08 +00:00
}
return options ;
2020-08-01 15:46:35 +00:00
}
2020-09-11 18:15:08 +00:00
save ( options ) {
2020-08-01 15:46:35 +00:00
return new Promise ( ( resolve , reject ) => {
chrome . storage . local . set ( { options : JSON . stringify ( options ) } , ( ) => {
const error = chrome . runtime . lastError ;
if ( error ) {
2020-09-11 18:15:08 +00:00
reject ( new Error ( error . message ) ) ;
2020-08-01 15:46:35 +00:00
} else {
resolve ( ) ;
}
2020-05-23 00:03:34 +00:00
} ) ;
2020-08-01 15:46:35 +00:00
} ) ;
}
2020-05-23 00:03:34 +00:00
2020-09-11 18:15:08 +00:00
getDefault ( ) {
2020-10-27 23:26:30 +00:00
const optionsVersion = this . _getVersionUpdates ( ) . length ;
2021-05-22 19:45:20 +00:00
const options = this . _optionsSchema . getValidValueOrDefault ( ) ;
2020-10-27 23:26:30 +00:00
options . version = optionsVersion ;
return options ;
2020-10-20 00:05:45 +00:00
}
2020-09-11 18:15:08 +00:00
createValidatingProxy ( options ) {
2021-05-22 19:45:20 +00:00
return this . _optionsSchema . createProxy ( options ) ;
2020-09-11 18:15:08 +00:00
}
validate ( options ) {
2021-05-22 19:45:20 +00:00
return this . _optionsSchema . validate ( options ) ;
2020-08-01 15:46:35 +00:00
}
// Legacy profile updating
2020-09-11 18:15:08 +00:00
_legacyProfileUpdateGetUpdates ( ) {
2020-08-01 15:46:35 +00:00
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 ;
2020-05-23 00:03:34 +00:00
}
2020-08-01 15:46:35 +00:00
] ;
}
2020-05-02 16:50:16 +00:00
2020-09-11 18:15:08 +00:00
_legacyProfileUpdateGetDefaults ( ) {
2020-08-01 15:46:35 +00:00
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
}
} ;
2019-09-06 22:21:20 +00:00
}
2019-09-05 23:56:29 +00:00
2020-09-11 18:15:08 +00:00
_legacyProfileUpdateAssignDefaults ( options ) {
2020-08-01 15:46:35 +00:00
const defaults = this . _legacyProfileUpdateGetDefaults ( ) ;
2017-07-16 20:14:28 +00:00
2020-08-01 15:46:35 +00:00
const combine = ( target , source ) => {
for ( const key in source ) {
2021-01-08 02:36:20 +00:00
if ( ! Object . prototype . hasOwnProperty . call ( target , key ) ) {
2020-08-01 15:46:35 +00:00
target [ key ] = source [ key ] ;
}
2017-07-16 20:14:28 +00:00
}
2020-08-01 15:46:35 +00:00
} ;
2017-07-16 20:14:28 +00:00
2020-08-01 15:46:35 +00:00
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 ) ;
2017-07-16 20:14:28 +00:00
2020-08-01 15:46:35 +00:00
return options ;
}
2019-09-07 23:50:58 +00:00
2020-09-11 18:15:08 +00:00
_legacyProfileUpdateUpdateVersion ( options ) {
2020-08-01 15:46:35 +00:00
const updates = this . _legacyProfileUpdateGetUpdates ( ) ;
this . _legacyProfileUpdateAssignDefaults ( options ) ;
2019-09-07 23:50:58 +00:00
2020-08-01 15:46:35 +00:00
const targetVersion = updates . length ;
const currentVersion = options . version ;
2019-09-07 23:50:58 +00:00
2020-08-01 15:46:35 +00:00
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 ) ;
}
2019-11-24 03:54:06 +00:00
}
2020-08-01 15:46:35 +00:00
}
2019-09-07 23:50:58 +00:00
2020-08-01 15:46:35 +00:00
options . version = targetVersion ;
return options ;
2019-09-07 23:50:58 +00:00
}
2020-08-01 15:46:35 +00:00
// Private
2019-09-07 23:50:58 +00:00
2021-01-29 02:17:10 +00:00
async _applyAnkiFieldTemplatesPatch ( options , modificationsUrl ) {
let patch = null ;
2020-09-06 18:39:18 +00:00
for ( const { options : profileOptions } of options . profiles ) {
const fieldTemplates = profileOptions . anki . fieldTemplates ;
2021-01-29 02:17:10 +00:00
if ( fieldTemplates === null ) { continue ; }
if ( patch === null ) {
const content = await this . _fetchAsset ( modificationsUrl ) ;
if ( this . _templatePatcher === null ) {
this . _templatePatcher = new TemplatePatcher ( ) ;
2020-09-06 18:39:18 +00:00
}
2021-01-29 02:17:10 +00:00
patch = this . _templatePatcher . parsePatch ( content ) ;
2020-09-06 18:39:18 +00:00
}
2021-01-29 02:17:10 +00:00
profileOptions . anki . fieldTemplates = this . _templatePatcher . applyPatch ( fieldTemplates , patch ) ;
2020-09-06 18:39:18 +00:00
}
}
2020-09-11 18:15:08 +00:00
async _fetchAsset ( url , json = false ) {
2020-09-06 18:39:18 +00:00
url = chrome . runtime . getURL ( url ) ;
const response = await fetch ( url , {
method : 'GET' ,
mode : 'no-cors' ,
cache : 'default' ,
credentials : 'omit' ,
redirect : 'follow' ,
referrerPolicy : 'no-referrer'
} ) ;
2020-09-11 18:15:08 +00:00
if ( ! response . ok ) {
throw new Error ( ` Failed to fetch ${ url } : ${ response . status } ` ) ;
}
return await ( json ? response . json ( ) : response . text ( ) ) ;
2020-09-06 18:39:18 +00:00
}
2020-09-11 18:15:08 +00:00
_getStringHashCode ( string ) {
2020-08-01 15:46:35 +00:00
let hashCode = 0 ;
2019-09-07 23:50:58 +00:00
2020-08-01 15:46:35 +00:00
if ( typeof string !== 'string' ) { return hashCode ; }
2019-09-07 23:50:58 +00:00
2020-08-01 15:46:35 +00:00
for ( let i = 0 , charCode = string . charCodeAt ( i ) ; i < string . length ; charCode = string . charCodeAt ( ++ i ) ) {
hashCode = ( ( hashCode << 5 ) - hashCode ) + charCode ;
hashCode |= 0 ;
2019-09-10 00:19:49 +00:00
}
2019-09-07 23:50:58 +00:00
2020-08-01 15:46:35 +00:00
return hashCode ;
2019-11-24 03:54:06 +00:00
}
2020-09-11 18:15:08 +00:00
async _applyUpdates ( options , updates ) {
2020-08-01 15:46:35 +00:00
const targetVersion = updates . length ;
let currentVersion = options . version ;
2017-07-16 20:14:28 +00:00
2020-08-01 15:46:35 +00:00
if ( typeof currentVersion !== 'number' || ! Number . isFinite ( currentVersion ) ) {
currentVersion = 0 ;
2019-09-06 00:35:04 +00:00
}
2017-07-16 20:14:28 +00:00
2020-08-01 15:46:35 +00:00
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 ;
}
2019-12-15 04:06:44 +00:00
2021-07-06 03:24:06 +00:00
_getVersionUpdates ( targetVersion ) {
const result = [
2021-01-16 20:35:21 +00:00
{ 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 ) } ,
2021-02-25 22:48:39 +00:00
{ async : true , update : this . _updateVersion8 . bind ( this ) } ,
2021-03-15 02:51:48 +00:00
{ async : false , update : this . _updateVersion9 . bind ( this ) } ,
2021-04-03 17:02:49 +00:00
{ async : true , update : this . _updateVersion10 . bind ( this ) } ,
2021-05-16 19:24:38 +00:00
{ async : false , update : this . _updateVersion11 . bind ( this ) } ,
2021-07-03 02:46:38 +00:00
{ async : true , update : this . _updateVersion12 . bind ( this ) } ,
2021-08-07 16:40:51 +00:00
{ async : true , update : this . _updateVersion13 . bind ( this ) } ,
2021-09-26 15:08:16 +00:00
{ async : false , update : this . _updateVersion14 . bind ( this ) } ,
2021-12-18 17:38:39 +00:00
{ async : false , update : this . _updateVersion15 . bind ( this ) } ,
2022-02-03 01:09:13 +00:00
{ async : false , update : this . _updateVersion16 . bind ( this ) } ,
{ async : false , update : this . _updateVersion17 . bind ( this ) }
2020-08-01 15:46:35 +00:00
] ;
2021-07-06 03:24:06 +00:00
if ( typeof targetVersion === 'number' && targetVersion < result . length ) {
result . splice ( targetVersion ) ;
}
return result ;
2020-08-01 15:46:35 +00:00
}
2020-08-01 20:23:33 +00:00
2020-09-11 18:15:08 +00:00
_updateVersion1 ( options ) {
2020-08-01 20:23:33 +00:00
// Version 1 changes:
// Added options.global.database.prefixWildcardsSupported = false.
options . global = {
database : {
prefixWildcardsSupported : false
}
} ;
return options ;
}
2020-09-11 18:15:08 +00:00
_updateVersion2 ( options ) {
2020-08-01 20:23:33 +00:00
// 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 ;
}
2020-09-11 18:15:08 +00:00
async _updateVersion3 ( options ) {
2020-08-01 20:23:33 +00:00
// Version 3 changes:
// Pitch accent Anki field templates added.
2021-02-13 00:56:24 +00:00
await this . _applyAnkiFieldTemplatesPatch ( options , '/data/templates/anki-field-templates-upgrade-v2.handlebars' ) ;
2020-08-01 20:23:33 +00:00
return options ;
}
2020-09-11 18:15:08 +00:00
async _updateVersion4 ( options ) {
2020-09-04 21:44:00 +00:00
// Version 4 changes:
// Options conditions converted to string representations.
2020-09-06 02:03:35 +00:00
// Added usePopupWindow.
2020-09-08 15:01:08 +00:00
// Updated handlebars templates to include "clipboard-image" definition.
2020-09-26 17:45:48 +00:00
// Updated handlebars templates to include "clipboard-text" definition.
2020-09-08 23:40:15 +00:00
// Added hideDelay.
2020-09-09 20:59:03 +00:00
// Added inputs to profileOptions.scanning.
2020-09-13 15:33:10 +00:00
// Added pointerEventsEnabled to profileOptions.scanning.
2020-09-26 23:24:21 +00:00
// Added preventMiddleMouse to profileOptions.scanning.
2020-09-04 21:44:00 +00:00
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 } `
) ;
}
}
}
2020-09-12 17:20:02 +00:00
const createInputDefaultOptions = ( ) => ( {
showAdvanced : false ,
2020-09-27 15:53:07 +00:00
searchTerms : true ,
searchKanji : true ,
2020-09-27 15:46:37 +00:00
scanOnTouchMove : true ,
2020-09-12 17:20:02 +00:00
scanOnPenHover : true ,
scanOnPenPress : true ,
2020-09-13 17:25:03 +00:00
scanOnPenRelease : false ,
2020-09-27 15:46:37 +00:00
preventTouchScrolling : true
2020-09-12 17:20:02 +00:00
} ) ;
2020-09-06 02:03:35 +00:00
for ( const { options : profileOptions } of options . profiles ) {
profileOptions . general . usePopupWindow = false ;
2020-09-08 23:40:15 +00:00
profileOptions . scanning . hideDelay = 0 ;
2020-09-13 15:33:10 +00:00
profileOptions . scanning . pointerEventsEnabled = false ;
2020-09-26 23:24:21 +00:00
profileOptions . scanning . preventMiddleMouse = {
onWebPages : false ,
onPopupPages : false ,
onSearchPages : false ,
onSearchQuery : false
} ;
2020-09-09 20:59:03 +00:00
2020-09-13 17:16:56 +00:00
const { modifier , middleMouse } = profileOptions . scanning ;
2020-09-20 15:30:38 +00:00
delete profileOptions . scanning . modifier ;
delete profileOptions . scanning . middleMouse ;
2020-09-09 20:59:03 +00:00
const scanningInputs = [ ] ;
2020-09-11 04:29:38 +00:00
let modifierInput = '' ;
2020-09-09 20:59:03 +00:00
switch ( modifier ) {
case 'alt' :
case 'ctrl' :
case 'shift' :
case 'meta' :
2020-09-11 04:29:38 +00:00
modifierInput = modifier ;
2020-09-09 20:59:03 +00:00
break ;
case 'none' :
2020-09-11 04:29:38 +00:00
modifierInput = '' ;
2020-09-09 20:59:03 +00:00
break ;
}
2020-09-11 04:29:38 +00:00
scanningInputs . push ( {
include : modifierInput ,
2020-09-26 21:51:52 +00:00
exclude : 'mouse0' ,
2020-09-12 17:20:02 +00:00
types : { mouse : true , touch : false , pen : false } ,
options : createInputDefaultOptions ( )
2020-09-11 04:29:38 +00:00
} ) ;
2020-09-09 20:59:03 +00:00
if ( middleMouse ) {
2020-09-11 04:29:38 +00:00
scanningInputs . push ( {
include : 'mouse2' ,
2020-09-11 18:13:52 +00:00
exclude : '' ,
2020-09-12 17:20:02 +00:00
types : { mouse : true , touch : false , pen : false } ,
options : createInputDefaultOptions ( )
2020-09-11 18:13:52 +00:00
} ) ;
}
2020-09-13 17:16:56 +00:00
scanningInputs . push ( {
include : '' ,
exclude : '' ,
types : { mouse : false , touch : true , pen : true } ,
options : createInputDefaultOptions ( )
} ) ;
2020-09-09 20:59:03 +00:00
profileOptions . scanning . inputs = scanningInputs ;
2020-09-06 02:03:35 +00:00
}
2021-02-13 00:56:24 +00:00
await this . _applyAnkiFieldTemplatesPatch ( options , '/data/templates/anki-field-templates-upgrade-v4.handlebars' ) ;
2020-09-04 21:44:00 +00:00
return options ;
}
2020-10-27 23:26:30 +00:00
_updateVersion5 ( options ) {
// Version 5 changes:
// Removed legacy version number from profile options.
for ( const profile of options . profiles ) {
delete profile . options . version ;
}
return options ;
}
2020-11-05 01:39:23 +00:00
async _updateVersion6 ( options ) {
// Version 6 changes:
// Updated handlebars templates to include "conjugation" definition.
2020-11-07 03:14:00 +00:00
// Added global option showPopupPreview.
2020-12-07 02:17:05 +00:00
// Added global option useSettingsV2.
2020-11-08 21:25:07 +00:00
// Added anki.checkForDuplicates.
2020-11-26 04:22:05 +00:00
// Added general.glossaryLayoutMode; removed general.compactGlossaries.
2021-02-13 00:56:24 +00:00
await this . _applyAnkiFieldTemplatesPatch ( options , '/data/templates/anki-field-templates-upgrade-v6.handlebars' ) ;
2020-11-07 03:14:00 +00:00
options . global . showPopupPreview = false ;
2020-12-07 02:17:05 +00:00
options . global . useSettingsV2 = false ;
2020-11-08 21:25:07 +00:00
for ( const profile of options . profiles ) {
profile . options . anki . checkForDuplicates = true ;
2020-11-26 04:22:05 +00:00
profile . options . general . glossaryLayoutMode = ( profile . options . general . compactGlossaries ? 'compact' : 'default' ) ;
delete profile . options . general . compactGlossaries ;
2020-11-13 01:34:11 +00:00
const fieldTemplates = profile . options . anki . fieldTemplates ;
if ( typeof fieldTemplates === 'string' ) {
profile . options . anki . fieldTemplates = this . _updateVersion6AnkiTemplatesCompactTags ( fieldTemplates ) ;
}
2020-11-08 21:25:07 +00:00
}
2020-11-05 01:39:23 +00:00
return options ;
}
2020-11-13 01:34:11 +00:00
_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 } } { { / s e t ~ } }
{ { ~ # if definitionTags ~ } } { { # each definitionTags ~ } }
{ { ~ # if ( op "||" ( op "!" . . / data . compactTags ) ( op "!" redundant ) ) ~ } }
{ { ~ # if ( get "any" ) } } , { { else } } < i > ( { { / i f ~ } }
{ { name } }
{ { ~ # set "any" true } } { { / s e t ~ } }
{ { ~ / i f ~ } }
{ { ~ / e a c h ~ } }
{ { ~ # if ( get "any" ) } } ) < / i > { { / i f ~ } }
{ { ~ / i f ~ } }
{ { ~ / s c o p e ~ } } `
) ;
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 ;
}
2020-12-18 16:24:43 +00:00
_updateVersion7 ( options ) {
// Version 7 changes:
// Added general.maximumClipboardSearchLength.
2020-12-19 20:42:44 +00:00
// Added general.popupCurrentIndicatorMode.
2020-12-20 01:07:55 +00:00
// Added general.popupActionBarVisibility.
// Added general.popupActionBarLocation.
2020-12-21 03:16:38 +00:00
// Removed global option showPopupPreview.
delete options . global . showPopupPreview ;
2020-12-18 16:24:43 +00:00
for ( const profile of options . profiles ) {
profile . options . general . maximumClipboardSearchLength = 1000 ;
2020-12-22 23:22:14 +00:00
profile . options . general . popupCurrentIndicatorMode = 'triangle' ;
2020-12-20 01:07:55 +00:00
profile . options . general . popupActionBarVisibility = 'auto' ;
profile . options . general . popupActionBarLocation = 'right' ;
2020-12-18 16:24:43 +00:00
}
return options ;
}
2021-01-03 17:12:55 +00:00
2021-01-16 20:29:42 +00:00
async _updateVersion8 ( options ) {
2021-01-03 17:12:55 +00:00
// Version 8 changes:
// Added translation.textReplacements.
2021-01-10 02:25:04 +00:00
// Moved anki.sentenceExt to sentenceParsing.scanExtent.
2021-01-10 19:43:06 +00:00
// Added sentenceParsing.enableTerminationCharacters.
// Added sentenceParsing.terminationCharacters.
2021-01-10 21:49:40 +00:00
// Changed general.popupActionBarLocation.
2021-01-15 03:42:11 +00:00
// Added inputs.hotkeys.
// Added anki.suspendNewCards.
2021-01-16 15:22:24 +00:00
// Added popupWindow.
2021-01-16 20:29:42 +00:00
// Updated handlebars templates to include "stroke-count" definition.
2021-01-16 21:55:40 +00:00
// Updated global.useSettingsV2 to be true (opt-out).
2021-01-26 03:05:06 +00:00
// Added audio.customSourceType.
2021-01-26 23:30:01 +00:00
// 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.
2021-02-13 00:56:24 +00:00
await this . _applyAnkiFieldTemplatesPatch ( options , '/data/templates/anki-field-templates-upgrade-v8.handlebars' ) ;
2021-01-16 21:55:40 +00:00
options . global . useSettingsV2 = true ;
2021-01-03 17:12:55 +00:00
for ( const profile of options . profiles ) {
profile . options . translation . textReplacements = {
searchOriginal : true ,
groups : [ ]
} ;
2021-01-10 02:25:04 +00:00
profile . options . sentenceParsing = {
2021-01-10 19:43:06 +00:00
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 }
]
2021-01-10 02:25:04 +00:00
} ;
delete profile . options . anki . sentenceExt ;
2021-01-10 21:49:40 +00:00
profile . options . general . popupActionBarLocation = 'top' ;
2021-01-15 01:56:18 +00:00
profile . options . inputs = {
hotkeys : [
2021-01-16 01:19:56 +00:00
{ action : 'close' , key : 'Escape' , modifiers : [ ] , scopes : [ 'popup' ] , enabled : true } ,
{ action : 'focusSearchBox' , key : 'Escape' , modifiers : [ ] , scopes : [ 'search' ] , enabled : true } ,
2021-01-15 01:56:18 +00:00
{ 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 } ,
2021-01-18 00:35:01 +00:00
{ action : 'copyHostSelection' , key : 'KeyC' , modifiers : [ 'ctrl' ] , scopes : [ 'popup' ] , enabled : true }
2021-01-15 01:56:18 +00:00
]
} ;
2021-01-15 03:42:11 +00:00
profile . options . anki . suspendNewCards = false ;
2021-01-16 15:22:24 +00:00
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'
} ;
2021-01-24 03:46:00 +00:00
profile . options . audio . customSourceType = 'audio' ;
2021-01-26 23:30:01 +00:00
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 ;
2021-01-03 17:12:55 +00:00
}
return options ;
}
2021-02-25 22:48:39 +00:00
_updateVersion9 ( options ) {
// Version 9 changes:
// Added general.frequencyDisplayMode.
2021-02-27 19:04:52 +00:00
// Added general.termDisplayMode.
2021-02-25 22:48:39 +00:00
for ( const profile of options . profiles ) {
profile . options . general . frequencyDisplayMode = 'split-tags-grouped' ;
2021-02-27 19:04:52 +00:00
profile . options . general . termDisplayMode = 'ruby' ;
2021-02-25 22:48:39 +00:00
}
return options ;
}
2021-03-15 02:51:48 +00:00
2021-03-26 23:50:54 +00:00
async _updateVersion10 ( options ) {
2021-03-15 02:51:48 +00:00
// Version 10 changes:
// Removed global option useSettingsV2.
2021-03-26 23:50:54 +00:00
// Added part-of-speech field template.
2021-03-28 02:30:45 +00:00
// Added an argument to hotkey inputs.
2021-03-31 22:17:28 +00:00
// Added definitionsCollapsible to dictionary options.
2021-03-26 23:50:54 +00:00
await this . _applyAnkiFieldTemplatesPatch ( options , '/data/templates/anki-field-templates-upgrade-v10.handlebars' ) ;
2021-03-15 02:51:48 +00:00
delete options . global . useSettingsV2 ;
2021-03-28 02:30:45 +00:00
for ( const profile of options . profiles ) {
2021-03-31 22:17:28 +00:00
for ( const dictionaryOptions of Object . values ( profile . options . dictionaries ) ) {
dictionaryOptions . definitionsCollapsible = 'not-collapsible' ;
}
2021-03-28 02:30:45 +00:00
for ( const hotkey of profile . options . inputs . hotkeys ) {
switch ( hotkey . action ) {
case 'previousEntry' :
hotkey . argument = '1' ;
break ;
case 'previousEntry3' :
hotkey . action = 'previousEntry' ;
hotkey . argument = '3' ;
break ;
case 'nextEntry' :
hotkey . argument = '1' ;
break ;
case 'nextEntry3' :
hotkey . action = 'nextEntry' ;
hotkey . argument = '3' ;
break ;
default :
hotkey . argument = '' ;
break ;
}
}
}
2021-03-15 02:51:48 +00:00
return options ;
}
2021-04-03 17:02:49 +00:00
_updateVersion11 ( options ) {
// Version 11 changes:
// Changed dictionaries to an array.
2021-04-04 20:22:35 +00:00
// Changed audio.customSourceUrl's {expression} marker to {term}.
2021-04-30 21:57:53 +00:00
// Added anki.displayTags.
2021-04-04 20:22:35 +00:00
const customSourceUrlPattern = /\{expression\}/g ;
2021-04-03 17:02:49 +00:00
for ( const profile of options . profiles ) {
const dictionariesNew = [ ] ;
for ( const [ name , { priority , enabled , allowSecondarySearches , definitionsCollapsible } ] of Object . entries ( profile . options . dictionaries ) ) {
dictionariesNew . push ( { name , priority , enabled , allowSecondarySearches , definitionsCollapsible } ) ;
}
profile . options . dictionaries = dictionariesNew ;
2021-04-04 20:22:35 +00:00
let { customSourceUrl } = profile . options . audio ;
if ( typeof customSourceUrl === 'string' ) {
customSourceUrl = customSourceUrl . replace ( customSourceUrlPattern , '{term}' ) ;
}
profile . options . audio . customSourceUrl = customSourceUrl ;
2021-04-30 21:57:53 +00:00
profile . options . anki . displayTags = 'never' ;
2021-04-03 17:02:49 +00:00
}
return options ;
}
2021-05-16 19:24:38 +00:00
2021-05-18 00:18:37 +00:00
async _updateVersion12 ( options ) {
2021-05-16 19:24:38 +00:00
// Version 12 changes:
// Changed sentenceParsing.enableTerminationCharacters to sentenceParsing.terminationCharacterMode.
2021-05-18 00:18:37 +00:00
// Added {search-query} field marker.
2021-05-27 00:38:15 +00:00
// Updated audio.sources[] to change 'custom' into 'custom-json'.
// Removed audio.customSourceType.
2021-05-18 00:18:37 +00:00
await this . _applyAnkiFieldTemplatesPatch ( options , '/data/templates/anki-field-templates-upgrade-v12.handlebars' ) ;
2021-05-16 19:24:38 +00:00
for ( const profile of options . profiles ) {
2021-05-27 00:38:15 +00:00
const { sentenceParsing , audio } = profile . options ;
2021-05-16 19:24:38 +00:00
sentenceParsing . terminationCharacterMode = sentenceParsing . enableTerminationCharacters ? 'custom' : 'newlines' ;
delete sentenceParsing . enableTerminationCharacters ;
2021-05-27 00:38:15 +00:00
2021-05-30 16:41:19 +00:00
const { sources , customSourceUrl , customSourceType , textToSpeechVoice } = audio ;
2021-05-27 00:38:15 +00:00
audio . sources = sources . map ( ( type ) => {
switch ( type ) {
2021-05-30 16:41:19 +00:00
case 'text-to-speech' :
case 'text-to-speech-reading' :
return { type , url : '' , voice : textToSpeechVoice } ;
2021-05-27 00:38:15 +00:00
case 'custom' :
2021-05-30 16:41:19 +00:00
return { type : ( customSourceType === 'json' ? 'custom-json' : 'custom' ) , url : customSourceUrl , voice : '' } ;
2021-05-27 00:38:15 +00:00
default :
2021-05-30 16:41:19 +00:00
return { type , url : '' , voice : '' } ;
2021-05-27 00:38:15 +00:00
}
} ) ;
delete audio . customSourceType ;
2021-05-30 16:41:19 +00:00
delete audio . customSourceUrl ;
delete audio . textToSpeechVoice ;
2021-05-16 19:24:38 +00:00
}
return options ;
}
2021-07-03 02:46:38 +00:00
async _updateVersion13 ( options ) {
// Version 13 changes:
// Handlebars templates updated to use formatGlossary.
2021-07-07 02:59:24 +00:00
// Handlebars templates updated to use new media format.
2021-07-08 00:00:30 +00:00
// Added {selection-text} field marker.
2021-07-09 21:48:27 +00:00
// Added {sentence-furigana} field marker.
2021-07-14 00:29:53 +00:00
// Added anki.duplicateScopeCheckAllModels.
2021-07-18 20:01:42 +00:00
// Updated pronunciation templates.
2021-07-03 02:46:38 +00:00
await this . _applyAnkiFieldTemplatesPatch ( options , '/data/templates/anki-field-templates-upgrade-v13.handlebars' ) ;
2021-07-14 00:29:53 +00:00
for ( const profile of options . profiles ) {
profile . options . anki . duplicateScopeCheckAllModels = false ;
}
2021-07-03 02:46:38 +00:00
return options ;
}
2021-08-07 16:40:51 +00:00
_updateVersion14 ( options ) {
// Version 14 changes:
// Added accessibility options.
for ( const profile of options . profiles ) {
profile . options . accessibility = {
forceGoogleDocsHtmlRendering : false
} ;
}
return options ;
}
2021-09-26 15:08:16 +00:00
_updateVersion15 ( options ) {
// Version 15 changes:
// Added general.sortFrequencyDictionary.
// Added general.sortFrequencyDictionaryOrder.
for ( const profile of options . profiles ) {
profile . options . general . sortFrequencyDictionary = null ;
profile . options . general . sortFrequencyDictionaryOrder = 'descending' ;
}
return options ;
}
2021-12-18 17:38:39 +00:00
_updateVersion16 ( options ) {
// Version 16 changes:
// Added scanning.matchTypePrefix.
for ( const profile of options . profiles ) {
profile . options . scanning . matchTypePrefix = false ;
}
return options ;
}
2022-02-03 01:09:13 +00:00
_updateVersion17 ( options ) {
// Version 17 changes:
// Added vertical sentence punctuation to terminationCharacters.
const additions = [ '︒' , '︕' , '︖' , '︙' ] ;
for ( const profile of options . profiles ) {
const { terminationCharacters } = profile . options . sentenceParsing ;
const newAdditions = [ ] ;
for ( const character of additions ) {
if ( terminationCharacters . findIndex ( ( value ) => ( value . character1 === character && value . character2 === null ) ) < 0 ) {
newAdditions . push ( character ) ;
}
}
for ( const character of newAdditions ) {
terminationCharacters . push ( {
enabled : true ,
character1 : character ,
character2 : null ,
includeCharacterAtStart : false ,
includeCharacterAtEnd : true
} ) ;
}
}
return options ;
}
2019-12-15 04:06:44 +00:00
}