diff --git a/ext/js/background/profile-conditions-util.js b/ext/js/background/profile-conditions-util.js index 928026e0..dcd60796 100644 --- a/ext/js/background/profile-conditions-util.js +++ b/ext/js/background/profile-conditions-util.js @@ -57,6 +57,17 @@ class ProfileConditionsUtil { ['notInclude', this._createSchemaModifierKeysNotInclude.bind(this)] ]) } + ], + [ + 'flags', + { + operators: new Map([ + ['are', this._createSchemaFlagsAre.bind(this)], + ['areNot', this._createSchemaFlagsAreNot.bind(this)], + ['include', this._createSchemaFlagsInclude.bind(this)], + ['notInclude', this._createSchemaFlagsNotInclude.bind(this)] + ]) + } ] ]); } @@ -121,6 +132,10 @@ class ProfileConditionsUtil { // NOP } } + const {flags} = normalizedContext; + if (!Array.isArray(flags)) { + normalizedContext.flags = []; + } return normalizedContext; } @@ -222,54 +237,76 @@ class ProfileConditionsUtil { // modifierKeys schema creation functions _createSchemaModifierKeysAre(value) { - return this._createSchemaModifierKeysGeneric(value, true, false); + return this._createSchemaArrayCheck('modifierKeys', value, true, false); } _createSchemaModifierKeysAreNot(value) { return { - not: [this._createSchemaModifierKeysGeneric(value, true, false)] + not: [this._createSchemaArrayCheck('modifierKeys', value, true, false)] }; } _createSchemaModifierKeysInclude(value) { - return this._createSchemaModifierKeysGeneric(value, false, false); + return this._createSchemaArrayCheck('modifierKeys', value, false, false); } _createSchemaModifierKeysNotInclude(value) { - return this._createSchemaModifierKeysGeneric(value, false, true); + return this._createSchemaArrayCheck('modifierKeys', value, false, true); } - _createSchemaModifierKeysGeneric(value, exact, none) { + // modifierKeys schema creation functions + + _createSchemaFlagsAre(value) { + return this._createSchemaArrayCheck('flags', value, true, false); + } + + _createSchemaFlagsAreNot(value) { + return { + not: [this._createSchemaArrayCheck('flags', value, true, false)] + }; + } + + _createSchemaFlagsInclude(value) { + return this._createSchemaArrayCheck('flags', value, false, false); + } + + _createSchemaFlagsNotInclude(value) { + return this._createSchemaArrayCheck('flags', value, false, true); + } + + // Generic + + _createSchemaArrayCheck(key, value, exact, none) { const containsList = []; - for (const modifierKey of this._split(value)) { - if (modifierKey.length === 0) { continue; } + for (const item of this._split(value)) { + if (item.length === 0) { continue; } containsList.push({ contains: { - const: modifierKey + const: item } }); } const containsListCount = containsList.length; - const modifierKeysSchema = { + const schema = { type: 'array' }; if (exact) { - modifierKeysSchema.maxItems = containsListCount; + schema.maxItems = containsListCount; } if (none) { if (containsListCount > 0) { - modifierKeysSchema.not = containsList; + schema.not = containsList; } } else { - modifierKeysSchema.minItems = containsListCount; + schema.minItems = containsListCount; if (containsListCount > 0) { - modifierKeysSchema.allOf = containsList; + schema.allOf = containsList; } } return { - required: ['modifierKeys'], + required: [key], properties: { - modifierKeys: modifierKeysSchema + [key]: schema } }; } diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js index f672909e..e60de796 100644 --- a/ext/js/display/search-display-controller.js +++ b/ext/js/display/search-display-controller.js @@ -179,12 +179,12 @@ class SearchDisplayController { e.preventDefault(); e.stopImmediatePropagation(); this._display.blurElement(e.currentTarget); - this._search(true, true, true); + this._search(true, true, true, null); } _onSearch(e) { e.preventDefault(); - this._search(true, true, true); + this._search(true, true, true, null); } _onCopy() { @@ -199,7 +199,7 @@ class SearchDisplayController { } this._queryInput.value = text; this._updateSearchHeight(true); - this._search(animate, false, autoSearchContent); + this._search(animate, false, autoSearchContent, ['clipboard']); } _onWanakanaEnableChange(e) { @@ -342,11 +342,15 @@ class SearchDisplayController { }); } - _search(animate, history, lookup) { + _search(animate, history, lookup, flags) { const query = this._queryInput.value; const depth = this._display.depth; const url = window.location.href; const documentTitle = document.title; + const optionsContext = {depth, url}; + if (flags !== null) { + optionsContext.flags = flags; + } const details = { focus: false, history, @@ -355,7 +359,7 @@ class SearchDisplayController { }, state: { focusEntry: 0, - optionsContext: {depth, url}, + optionsContext, url, sentence: {text: query, offset: 0}, documentTitle diff --git a/ext/js/pages/settings/profile-conditions-ui.js b/ext/js/pages/settings/profile-conditions-ui.js index 5fda1dc0..0a598693 100644 --- a/ext/js/pages/settings/profile-conditions-ui.js +++ b/ext/js/pages/settings/profile-conditions-ui.js @@ -30,6 +30,10 @@ class ProfileConditionsUI extends EventDispatcher { this._eventListeners = new EventListenerCollection(); this._defaultType = 'popupLevel'; this._profileIndex = 0; + const validateInteger = this._validateInteger.bind(this); + const normalizeInteger = this._normalizeInteger.bind(this); + const validateFlags = this._validateFlags.bind(this); + const normalizeFlags = this._normalizeFlags.bind(this); this._descriptors = new Map([ [ 'popupLevel', @@ -37,12 +41,12 @@ class ProfileConditionsUI extends EventDispatcher { displayName: 'Popup Level', defaultOperator: 'equal', operators: new Map([ - ['equal', {displayName: '=', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['notEqual', {displayName: '\u2260', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['lessThan', {displayName: '<', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['greaterThan', {displayName: '>', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['lessThanOrEqual', {displayName: '\u2264', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}], - ['greaterThanOrEqual', {displayName: '\u2265', type: 'integer', defaultValue: '0', validate: this._validateInteger.bind(this), normalize: this._normalizeInteger.bind(this)}] + ['equal', {displayName: '=', type: 'integer', defaultValue: '0', validate: validateInteger, normalize: normalizeInteger}], + ['notEqual', {displayName: '\u2260', type: 'integer', defaultValue: '0', validate: validateInteger, normalize: normalizeInteger}], + ['lessThan', {displayName: '<', type: 'integer', defaultValue: '0', validate: validateInteger, normalize: normalizeInteger}], + ['greaterThan', {displayName: '>', type: 'integer', defaultValue: '0', validate: validateInteger, normalize: normalizeInteger}], + ['lessThanOrEqual', {displayName: '\u2264', type: 'integer', defaultValue: '0', validate: validateInteger, normalize: normalizeInteger}], + ['greaterThanOrEqual', {displayName: '\u2265', type: 'integer', defaultValue: '0', validate: validateInteger, normalize: normalizeInteger}] ]) } ], @@ -69,8 +73,24 @@ class ProfileConditionsUI extends EventDispatcher { ['notInclude', {displayName: 'Don\'t Include', type: 'modifierKeys', defaultValue: ''}] ]) } + ], + [ + 'flags', + { + displayName: 'Flags', + defaultOperator: 'are', + operators: new Map([ + ['are', {displayName: 'Are', type: 'string', defaultValue: '', validate: validateFlags, normalize: normalizeFlags}], + ['areNot', {displayName: 'Are Not', type: 'string', defaultValue: '', validate: validateFlags, normalize: normalizeFlags}], + ['include', {displayName: 'Include', type: 'string', defaultValue: '', validate: validateFlags, normalize: normalizeFlags}], + ['notInclude', {displayName: 'Don\'t Include', type: 'string', defaultValue: '', validate: validateFlags, normalize: normalizeFlags}] + ]) + } ] ]); + this._validFlags = new Set([ + 'clipboard' + ]); } get settingsController() { @@ -280,6 +300,20 @@ class ProfileConditionsUI extends EventDispatcher { return this.splitValue(value).join(', '); } + _validateFlags(value) { + const flags = this.splitValue(value); + for (const flag of flags) { + if (!this._validFlags.has(flag)) { + return false; + } + } + return flags.length > 0; + } + + _normalizeFlags(value) { + return [...new Set(this.splitValue(value))].join(', '); + } + _triggerConditionGroupCountChanged(count) { this.trigger('conditionGroupCountChanged', {count, profileIndex: this._profileIndex}); } diff --git a/test/test-profile-conditions-util.js b/test/test-profile-conditions-util.js index ebc72be7..b77578de 100644 --- a/test/test-profile-conditions-util.js +++ b/test/test-profile-conditions-util.js @@ -40,25 +40,25 @@ function testNormalizeContext() { // Empty { context: {}, - expected: {} + expected: {flags: []} }, // Domain normalization { context: {url: ''}, - expected: {url: ''} + expected: {url: '', flags: []} }, { context: {url: 'http://example.com/'}, - expected: {url: 'http://example.com/', domain: 'example.com'} + expected: {url: 'http://example.com/', domain: 'example.com', flags: []} }, { context: {url: 'http://example.com:1234/'}, - expected: {url: 'http://example.com:1234/', domain: 'example.com'} + expected: {url: 'http://example.com:1234/', domain: 'example.com', flags: []} }, { context: {url: 'http://user@example.com:1234/'}, - expected: {url: 'http://user@example.com:1234/', domain: 'example.com'} + expected: {url: 'http://user@example.com:1234/', domain: 'example.com', flags: []} } ]; @@ -611,6 +611,266 @@ function testSchemas() { ] }, + // flags tests + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'are', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 0, + minItems: 0 + } + } + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: false, context: {flags: ['test1']}}, + {expected: false, context: {flags: ['test1', 'test2']}}, + {expected: false, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'are', + value: 'test1, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'test1'}}, + {contains: {const: 'test2'}} + ] + } + } + }, + inputs: [ + {expected: false, context: {}}, + {expected: false, context: {flags: []}}, + {expected: false, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: false, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'areNot', + value: '' + } + ] + } + ], + expectedSchema: { + not: [ + { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 0, + minItems: 0 + } + } + } + ] + }, + inputs: [ + {expected: false, context: {}}, + {expected: false, context: {flags: []}}, + {expected: true, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'areNot', + value: 'test1, test2' + } + ] + } + ], + expectedSchema: { + not: [ + { + required: ['flags'], + properties: { + flags: { + type: 'array', + maxItems: 2, + minItems: 2, + allOf: [ + {contains: {const: 'test1'}}, + {contains: {const: 'test2'}} + ] + } + } + } + ] + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: true, context: {flags: ['test1']}}, + {expected: false, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'include', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + minItems: 0 + } + } + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: true, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'include', + value: 'test1, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + minItems: 2, + allOf: [ + {contains: {const: 'test1'}}, + {contains: {const: 'test2'}} + ] + } + } + }, + inputs: [ + {expected: false, context: {}}, + {expected: false, context: {flags: []}}, + {expected: false, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'notInclude', + value: '' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array' + } + } + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: true, context: {flags: ['test1']}}, + {expected: true, context: {flags: ['test1', 'test2']}}, + {expected: true, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + { + conditionGroups: [ + { + conditions: [ + { + type: 'flags', + operator: 'notInclude', + value: 'test1, test2' + } + ] + } + ], + expectedSchema: { + required: ['flags'], + properties: { + flags: { + type: 'array', + not: [ + {contains: {const: 'test1'}}, + {contains: {const: 'test2'}} + ] + } + } + }, + inputs: [ + {expected: true, context: {}}, + {expected: true, context: {flags: []}}, + {expected: false, context: {flags: ['test1']}}, + {expected: false, context: {flags: ['test1', 'test2']}}, + {expected: false, context: {flags: ['test1', 'test2', 'test3']}} + ] + }, + // Multiple conditions tests { conditionGroups: [