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 @@
-
+