diff --git a/ext/bg/background.html b/ext/bg/background.html index 218e9925..b3067b1e 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -38,7 +38,7 @@ - + diff --git a/ext/bg/css/settings.css b/ext/bg/css/settings.css index 1b945310..2abcc1b1 100644 --- a/ext/bg/css/settings.css +++ b/ext/bg/css/settings.css @@ -68,39 +68,39 @@ html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group content: "AND"; } -.input-group .condition-prefix { +.condition-prefix { flex: 0 0 auto; } -.input-group .condition-prefix, -.input-group .condition-group-separator-label { +.condition-prefix, +.condition-group-separator-label { width: 60px; text-align: center; } -.input-group .condition-group-separator-label { +.condition-group-separator-label { padding: 6px 12px; font-weight: bold; display: inline-block; } -.input-group .condition-type, -.input-group .condition-operator { +.condition-type, +.condition-operator { width: auto; text-align: center; text-align-last: center; } -.condition-group>.condition>*:first-child, +.condition-list>.condition>*:first-child, .audio-source-list>.audio-source>*:first-child { border-bottom-left-radius: 0; } -.condition-group>.condition:nth-child(n+2)>*:first-child, +.condition-list>.condition:nth-child(n+2)>*:first-child, .audio-source-list>.audio-source:nth-child(n+2)>*:first-child { border-top-left-radius: 0; } -.condition-group>.condition:nth-child(n+2)>div:last-child>button, +.condition-list>.condition:nth-child(n+2)>div:last-child>button, .audio-source-list>.audio-source:nth-child(n+2)>*:last-child>button { border-top-right-radius: 0; } -.condition-group>.condition:nth-last-child(n+2)>div:last-child>button, +.condition-list>.condition:nth-last-child(n+2)>div:last-child>button, .audio-source-list>.audio-source:nth-last-child(n+2)>*:last-child>button { border-bottom-right-radius: 0; } @@ -110,7 +110,7 @@ html:root:not([data-options-general-result-output-mode=merge]) #dict-main-group border-top-right-radius: 0; } -.condition-groups>*:last-of-type { +.condition-groups>.condition-group:last-child>.condition-group-separator-label { display: none; } diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js index 810370c4..7f85d9a5 100644 --- a/ext/bg/js/backend.js +++ b/ext/bg/js/backend.js @@ -28,13 +28,11 @@ * Mecab * ObjectPropertyAccessor * OptionsUtil + * ProfileConditions * RequestBuilder * TemplateRenderer * Translator - * conditionsTestValue * jp - * profileConditionsDescriptor - * profileConditionsDescriptorPromise */ class Backend { @@ -49,6 +47,8 @@ class Backend { this._options = null; this._optionsSchema = null; this._optionsSchemaValidator = new JsonSchemaValidator(); + this._profileConditionsSchemaCache = []; + this._profileConditionsUtil = new ProfileConditions(); this._defaultAnkiFieldTemplates = null; this._requestBuilder = new RequestBuilder(); this._audioUriBuilder = new AudioUriBuilder({ @@ -200,8 +200,6 @@ class Backend { } await this._translator.prepare(); - await profileConditionsDescriptorPromise; - this._optionsSchema = await this._fetchAsset('/bg/data/options-schema.json', true); this._defaultAnkiFieldTemplates = (await this._fetchAsset('/bg/data/default-anki-field-templates.handlebars')).trim(); this._options = await OptionsUtil.load(); @@ -397,6 +395,7 @@ class Backend { } async _onApiOptionsSave({source}) { + this._clearProfileConditionsSchemaCache(); const options = this.getFullOptions(); await OptionsUtil.save(options); this._applyOptions(source); @@ -1006,35 +1005,32 @@ class Backend { } _getProfileFromContext(options, optionsContext) { + optionsContext = this._profileConditionsUtil.normalizeContext(optionsContext); + + let index = 0; for (const profile of options.profiles) { const conditionGroups = profile.conditionGroups; - if (conditionGroups.length > 0 && this._testConditionGroups(conditionGroups, optionsContext)) { + + let schema; + if (index < this._profileConditionsSchemaCache.length) { + schema = this._profileConditionsSchemaCache[index]; + } else { + schema = this._profileConditionsUtil.createSchema(conditionGroups); + this._profileConditionsSchemaCache.push(schema); + } + + if (conditionGroups.length > 0 && this._optionsSchemaValidator.isValid(optionsContext, schema)) { return profile; } + ++index; } + return null; } - _testConditionGroups(conditionGroups, data) { - if (conditionGroups.length === 0) { return false; } - - for (const conditionGroup of conditionGroups) { - const conditions = conditionGroup.conditions; - if (conditions.length > 0 && this._testConditions(conditions, data)) { - return true; - } - } - - return false; - } - - _testConditions(conditions, data) { - for (const condition of conditions) { - if (!conditionsTestValue(profileConditionsDescriptor, condition.type, condition.operator, condition.value, data)) { - return false; - } - } - return true; + _clearProfileConditionsSchemaCache() { + this._profileConditionsSchemaCache = []; + this._optionsSchemaValidator.clearCache(); } _checkLastError() { diff --git a/ext/bg/js/json-schema.js b/ext/bg/js/json-schema.js index 30446559..1be78fd2 100644 --- a/ext/bg/js/json-schema.js +++ b/ext/bg/js/json-schema.js @@ -181,6 +181,10 @@ class JsonSchemaValidator { return this._getPropertySchema(schema, property, value, null); } + clearCache() { + this._regexCache.clear(); + } + // Private _getPropertySchema(schema, property, value, path) { diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 0d83f428..c513f572 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -389,6 +389,10 @@ class OptionsUtil { { async: true, update: this._updateVersion3.bind(this) + }, + { + async: false, + update: this._updateVersion4.bind(this) } ]; } @@ -459,4 +463,22 @@ class OptionsUtil { } return fieldTemplates; } + + static _updateVersion4(options) { + // Version 4 changes: + // Options conditions converted to string representations. + 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}` + ); + } + } + } + return options; + } } diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js deleted file mode 100644 index f3a85cb1..00000000 --- a/ext/bg/js/profile-conditions.js +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2019-2020 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 . - */ - -/* global - * Environment - */ - -let profileConditionsDescriptor = null; - -const profileConditionsDescriptorPromise = (async () => { - function profileConditionTestDomain(urlDomain, domain) { - return ( - urlDomain.endsWith(domain) && - ( - domain.length === urlDomain.length || - urlDomain[urlDomain.length - domain.length - 1] === '.' - ) - ); - } - - function profileConditionTestDomainList(url, domainList) { - const urlDomain = new URL(url).hostname.toLowerCase(); - for (const domain of domainList) { - if (profileConditionTestDomain(urlDomain, domain)) { - return true; - } - } - return false; - } - - const environment = new Environment(); - await environment.prepare(); - - const modifiers = environment.getInfo().modifiers; - const modifierSeparator = modifiers.separator; - const modifierKeyValues = modifiers.keys.map( - ({value, name}) => ({optionValue: value, name}) - ); - - const modifierValueToName = new Map( - modifierKeyValues.map(({optionValue, name}) => [optionValue, name]) - ); - - const modifierNameToValue = new Map( - modifierKeyValues.map(({optionValue, name}) => [name, optionValue]) - ); - - profileConditionsDescriptor = { - popupLevel: { - name: 'Popup Level', - description: 'Use profile depending on the level of the popup.', - placeholder: 'Number', - type: 'number', - step: 1, - defaultValue: 0, - defaultOperator: 'equal', - transform: (optionValue) => parseInt(optionValue, 10), - transformReverse: (transformedOptionValue) => `${transformedOptionValue}`, - validateTransformed: (transformedOptionValue) => Number.isFinite(transformedOptionValue), - operators: { - equal: { - name: '=', - test: ({depth}, optionValue) => (depth === optionValue) - }, - notEqual: { - name: '\u2260', - test: ({depth}, optionValue) => (depth !== optionValue) - }, - lessThan: { - name: '<', - test: ({depth}, optionValue) => (depth < optionValue) - }, - greaterThan: { - name: '>', - test: ({depth}, optionValue) => (depth > optionValue) - }, - lessThanOrEqual: { - name: '\u2264', - test: ({depth}, optionValue) => (depth <= optionValue) - }, - greaterThanOrEqual: { - name: '\u2265', - test: ({depth}, optionValue) => (depth >= optionValue) - } - } - }, - url: { - name: 'URL', - description: 'Use profile depending on the URL of the current website.', - defaultOperator: 'matchDomain', - operators: { - matchDomain: { - name: 'Matches Domain', - placeholder: 'Comma separated list of domains', - defaultValue: 'example.com', - transformCache: {}, - transform: (optionValue) => optionValue.split(/[,;\s]+/).map((v) => v.trim().toLowerCase()).filter((v) => v.length > 0), - transformReverse: (transformedOptionValue) => transformedOptionValue.join(', '), - validateTransformed: (transformedOptionValue) => (transformedOptionValue.length > 0), - test: ({url}, transformedOptionValue) => profileConditionTestDomainList(url, transformedOptionValue) - }, - matchRegExp: { - name: 'Matches RegExp', - placeholder: 'Regular expression', - defaultValue: 'example\\.com', - transformCache: {}, - transform: (optionValue) => new RegExp(optionValue, 'i'), - transformReverse: (transformedOptionValue) => transformedOptionValue.source, - test: ({url}, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(url)) - } - } - }, - modifierKeys: { - name: 'Modifier Keys', - description: 'Use profile depending on the active modifier keys.', - values: modifierKeyValues, - defaultOperator: 'are', - operators: { - are: { - name: 'are', - placeholder: 'Press one or more modifier keys here', - defaultValue: [], - type: 'keyMulti', - keySeparator: modifierSeparator, - transformInput: (optionValue) => optionValue - .split(modifierSeparator) - .filter((v) => v.length > 0) - .map((v) => modifierNameToValue.get(v)), - transformReverse: (transformedOptionValue) => transformedOptionValue - .map((v) => modifierValueToName.get(v)) - .join(modifierSeparator), - test: ({modifierKeys}, optionValue) => areSetsEqual(new Set(modifierKeys), new Set(optionValue)) - }, - areNot: { - name: 'are not', - placeholder: 'Press one or more modifier keys here', - defaultValue: [], - type: 'keyMulti', - keySeparator: modifierSeparator, - transformInput: (optionValue) => optionValue - .split(modifierSeparator) - .filter((v) => v.length > 0) - .map((v) => modifierNameToValue.get(v)), - transformReverse: (transformedOptionValue) => transformedOptionValue - .map((v) => modifierValueToName.get(v)) - .join(modifierSeparator), - test: ({modifierKeys}, optionValue) => !areSetsEqual(new Set(modifierKeys), new Set(optionValue)) - }, - include: { - name: 'include', - type: 'select', - defaultValue: 'alt', - test: ({modifierKeys}, optionValue) => modifierKeys.includes(optionValue) - }, - notInclude: { - name: 'don\'t include', - type: 'select', - defaultValue: 'alt', - test: ({modifierKeys}, optionValue) => !modifierKeys.includes(optionValue) - } - } - } - }; -})(); diff --git a/ext/bg/js/settings/conditions-ui.js b/ext/bg/js/settings/conditions-ui.js deleted file mode 100644 index 98b3d432..00000000 --- a/ext/bg/js/settings/conditions-ui.js +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Copyright (C) 2019-2020 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 . - */ - -/* global - * DocumentUtil - * conditionsNormalizeOptionValue - */ - -class ConditionsUI { - static instantiateTemplate(templateSelector) { - const template = document.querySelector(templateSelector); - const content = document.importNode(template.content, true); - return $(content.firstChild); - } -} - -ConditionsUI.Container = class Container { - constructor(conditionDescriptors, conditionNameDefault, conditionGroups, container, addButton) { - this.children = []; - this.conditionDescriptors = conditionDescriptors; - this.conditionNameDefault = conditionNameDefault; - this.conditionGroups = conditionGroups; - this.container = container; - this.addButton = addButton; - - this.container.empty(); - - for (const conditionGroup of toIterable(conditionGroups)) { - this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup)); - } - - this.addButton.on('click', this.onAddConditionGroup.bind(this)); - } - - cleanup() { - for (const child of this.children) { - child.cleanup(); - } - - this.addButton.off('click'); - this.container.empty(); - } - - save() { - // Override - } - - isolate(object) { - // Override - return object; - } - - remove(child) { - const index = this.children.indexOf(child); - if (index < 0) { - return; - } - - child.cleanup(); - this.children.splice(index, 1); - this.conditionGroups.splice(index, 1); - } - - onAddConditionGroup() { - const conditionGroup = this.isolate({ - conditions: [this.createDefaultCondition(this.conditionNameDefault)] - }); - this.conditionGroups.push(conditionGroup); - this.save(); - this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup)); - } - - createDefaultCondition(type) { - let operator = ''; - let value = ''; - if (hasOwn(this.conditionDescriptors, type)) { - const conditionDescriptor = this.conditionDescriptors[type]; - operator = conditionDescriptor.defaultOperator; - ({value} = this.getOperatorDefaultValue(type, operator)); - if (typeof value === 'undefined') { - value = ''; - } - } - return {type, operator, value}; - } - - getOperatorDefaultValue(type, operator) { - if (hasOwn(this.conditionDescriptors, type)) { - const conditionDescriptor = this.conditionDescriptors[type]; - if (hasOwn(conditionDescriptor.operators, operator)) { - const operatorDescriptor = conditionDescriptor.operators[operator]; - if (hasOwn(operatorDescriptor, 'defaultValue')) { - return {value: this.isolate(operatorDescriptor.defaultValue), fromOperator: true}; - } - } - if (hasOwn(conditionDescriptor, 'defaultValue')) { - return {value: this.isolate(conditionDescriptor.defaultValue), fromOperator: false}; - } - } - return {fromOperator: false}; - } -}; - -ConditionsUI.ConditionGroup = class ConditionGroup { - constructor(parent, conditionGroup) { - this.parent = parent; - this.children = []; - this.conditionGroup = conditionGroup; - this.container = $('
').addClass('condition-group').appendTo(parent.container); - this.options = ConditionsUI.instantiateTemplate('#condition-group-options-template').appendTo(parent.container); - this.separator = ConditionsUI.instantiateTemplate('#condition-group-separator-template').appendTo(parent.container); - this.addButton = this.options.find('.condition-add'); - - for (const condition of toIterable(conditionGroup.conditions)) { - this.children.push(new ConditionsUI.Condition(this, condition)); - } - - this.addButton.on('click', this.onAddCondition.bind(this)); - } - - cleanup() { - for (const child of this.children) { - child.cleanup(); - } - - this.addButton.off('click'); - this.container.remove(); - this.options.remove(); - this.separator.remove(); - } - - save() { - this.parent.save(); - } - - isolate(object) { - return this.parent.isolate(object); - } - - remove(child) { - const index = this.children.indexOf(child); - if (index < 0) { - return; - } - - child.cleanup(); - this.children.splice(index, 1); - this.conditionGroup.conditions.splice(index, 1); - - if (this.children.length === 0) { - this.parent.remove(this, false); - } - } - - onAddCondition() { - const condition = this.isolate(this.parent.createDefaultCondition(this.parent.conditionNameDefault)); - this.conditionGroup.conditions.push(condition); - this.children.push(new ConditionsUI.Condition(this, condition)); - } -}; - -ConditionsUI.Condition = class Condition { - constructor(parent, condition) { - this.parent = parent; - this.condition = condition; - this.container = ConditionsUI.instantiateTemplate('#condition-template').appendTo(parent.container); - this.input = this.container.find('.condition-input'); - this.inputInner = null; - this.typeSelect = this.container.find('.condition-type'); - this.operatorSelect = this.container.find('.condition-operator'); - this.removeButton = this.container.find('.condition-remove'); - - this.updateTypes(); - this.updateOperators(); - this.updateInput(); - - this.typeSelect.on('change', this.onConditionTypeChanged.bind(this)); - this.operatorSelect.on('change', this.onConditionOperatorChanged.bind(this)); - this.removeButton.on('click', this.onRemoveClicked.bind(this)); - } - - cleanup() { - this.inputInner.off('change'); - this.typeSelect.off('change'); - this.operatorSelect.off('change'); - this.removeButton.off('click'); - this.container.remove(); - } - - save() { - this.parent.save(); - } - - isolate(object) { - return this.parent.isolate(object); - } - - updateTypes() { - const conditionDescriptors = this.parent.parent.conditionDescriptors; - const optionGroup = this.typeSelect.find('optgroup'); - optionGroup.empty(); - for (const type of Object.keys(conditionDescriptors)) { - const conditionDescriptor = conditionDescriptors[type]; - $('
+ - - - - -
@@ -1143,7 +1141,6 @@ - @@ -1153,11 +1150,11 @@ - +