diff options
Diffstat (limited to 'ext')
| -rw-r--r-- | ext/bg/background.html | 2 | ||||
| -rw-r--r-- | ext/bg/js/conditions-ui.js | 317 | ||||
| -rw-r--r-- | ext/bg/js/conditions.js | 117 | ||||
| -rw-r--r-- | ext/bg/js/options.js | 22 | ||||
| -rw-r--r-- | ext/bg/js/profile-conditions.js | 85 | ||||
| -rw-r--r-- | ext/bg/js/settings-profiles.js | 14 | ||||
| -rw-r--r-- | ext/bg/settings.html | 89 | 
7 files changed, 644 insertions, 2 deletions
| diff --git a/ext/bg/background.html b/ext/bg/background.html index 90a56024..3b37db87 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -16,11 +16,13 @@          <script src="/bg/js/api.js"></script>          <script src="/bg/js/audio.js"></script>          <script src="/bg/js/backend-api-forwarder.js"></script> +        <script src="/bg/js/conditions.js"></script>          <script src="/bg/js/database.js"></script>          <script src="/bg/js/deinflector.js"></script>          <script src="/bg/js/dictionary.js"></script>          <script src="/bg/js/handlebars.js"></script>          <script src="/bg/js/options.js"></script> +        <script src="/bg/js/profile-conditions.js"></script>          <script src="/bg/js/request.js"></script>          <script src="/bg/js/templates.js"></script>          <script src="/bg/js/translator.js"></script> diff --git a/ext/bg/js/conditions-ui.js b/ext/bg/js/conditions-ui.js new file mode 100644 index 00000000..9b161c95 --- /dev/null +++ b/ext/bg/js/conditions-ui.js @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 <http://www.gnu.org/licenses/>. + */ + + +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 conditionGroups) { +            this.children.push(new ConditionsUI.ConditionGroup(this, conditionGroup)); +        } + +        this.addButton.on('click', () => this.onAddConditionGroup()); +    } + +    cleanup() { +        for (const child of this.children) { +            child.cleanup(); +        } + +        this.addButton.off('click'); +        this.container.empty(); +    } + +    save() { +        // Override +    } + +    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 = { +            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 (this.conditionDescriptors.hasOwnProperty(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 (this.conditionDescriptors.hasOwnProperty(type)) { +            const conditionDescriptor = this.conditionDescriptors[type]; +            if (conditionDescriptor.operators.hasOwnProperty(operator)) { +                const operatorDescriptor = conditionDescriptor.operators[operator]; +                if (operatorDescriptor.hasOwnProperty('defaultValue')) { +                    return {value: operatorDescriptor.defaultValue, fromOperator: true}; +                } +            } +            if (conditionDescriptor.hasOwnProperty('defaultValue')) { +                return {value: conditionDescriptor.defaultValue, fromOperator: false}; +            } +        } +        return {fromOperator: false}; +    } +}; + +ConditionsUI.ConditionGroup = class ConditionGroup { +    constructor(parent, conditionGroup) { +        this.parent = parent; +        this.children = []; +        this.conditionGroup = conditionGroup; +        this.container = $('<div>').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 conditionGroup.conditions) { +            this.children.push(new ConditionsUI.Condition(this, condition)); +        } + +        this.addButton.on('click', () => this.onAddCondition()); +    } + +    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(); +    } + +    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.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('input'); +        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.input.on('change', () => this.onInputChanged()); +        this.typeSelect.on('change', () => this.onConditionTypeChanged()); +        this.operatorSelect.on('change', () => this.onConditionOperatorChanged()); +        this.removeButton.on('click', () => this.onRemoveClicked()); +    } + +    cleanup() { +        this.input.off('change'); +        this.typeSelect.off('change'); +        this.operatorSelect.off('change'); +        this.removeButton.off('click'); +        this.container.remove(); +    } + +    save() { +        this.parent.save(); +    } + +    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]; +            $('<option>').val(type).text(conditionDescriptor.name).appendTo(optionGroup); +        } +        this.typeSelect.val(this.condition.type); +    } + +    updateOperators() { +        const conditionDescriptors = this.parent.parent.conditionDescriptors; +        const optionGroup = this.operatorSelect.find('optgroup'); +        optionGroup.empty(); + +        const type = this.condition.type; +        if (conditionDescriptors.hasOwnProperty(type)) { +            const conditionDescriptor = conditionDescriptors[type]; +            const operators = conditionDescriptor.operators; +            for (const operatorName of Object.keys(operators)) { +                const operatorDescriptor = operators[operatorName]; +                $('<option>').val(operatorName).text(operatorDescriptor.name).appendTo(optionGroup); +            } +        } + +        this.operatorSelect.val(this.condition.operator); +    } + +    updateInput() { +        const conditionDescriptors = this.parent.parent.conditionDescriptors; +        const {type, operator} = this.condition; +        const props = { +            placeholder: '', +            type: 'text' +        }; + +        const objects = []; +        if (conditionDescriptors.hasOwnProperty(type)) { +            const conditionDescriptor = conditionDescriptors[type]; +            objects.push(conditionDescriptor); +            if (conditionDescriptor.operators.hasOwnProperty(operator)) { +                const operatorDescriptor = conditionDescriptor.operators[operator]; +                objects.push(operatorDescriptor); +            } +        } + +        for (const object of objects) { +            if (object.hasOwnProperty('placeholder')) { +                props.placeholder = object.placeholder; +            } +            if (object.type === 'number') { +                props.type = 'number'; +                for (const prop of ['step', 'min', 'max']) { +                    if (object.hasOwnProperty(prop)) { +                        props[prop] = object[prop]; +                    } +                } +            } +        } + +        for (const prop in props) { +            this.input.prop(prop, props[prop]); +        } + +        const {valid} = this.validateValue(this.condition.value); +        this.input.toggleClass('is-invalid', !valid); +        this.input.val(this.condition.value); +    } + +    validateValue(value) { +        const conditionDescriptors = this.parent.parent.conditionDescriptors; +        let valid = true; +        try { +            value = conditionsNormalizeOptionValue( +                conditionDescriptors, +                this.condition.type, +                this.condition.operator, +                value +            ); +        } catch (e) { +            valid = false; +        } +        return {valid, value}; +    } + +    onInputChanged() { +        const {valid, value} = this.validateValue(this.input.val()); +        this.input.toggleClass('is-invalid', !valid); +        this.input.val(value); +        this.condition.value = value; +        this.save(); +    } + +    onConditionTypeChanged() { +        const type = this.typeSelect.val(); +        const {operator, value} = this.parent.parent.createDefaultCondition(type); +        this.condition.type = type; +        this.condition.operator = operator; +        this.condition.value = value; +        this.save(); +        this.updateOperators(); +        this.updateInput(); +    } + +    onConditionOperatorChanged() { +        const type = this.condition.type; +        const operator = this.operatorSelect.val(); +        const {value, fromOperator} = this.parent.parent.getOperatorDefaultValue(type, operator); +        this.condition.operator = operator; +        if (fromOperator) { +            this.condition.value = value; +        } +        this.save(); +        this.updateInput(); +    } + +    onRemoveClicked() { +        this.parent.remove(this); +        this.save(); +    } +}; diff --git a/ext/bg/js/conditions.js b/ext/bg/js/conditions.js new file mode 100644 index 00000000..ed4b14f5 --- /dev/null +++ b/ext/bg/js/conditions.js @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 <http://www.gnu.org/licenses/>. + */ + + +function conditionsValidateOptionValue(object, value) { +    if (object.hasOwnProperty('validate') && !object.validate(value)) { +        throw new Error('Invalid value for condition'); +    } + +    if (object.hasOwnProperty('transform')) { +        value = object.transform(value); + +        if (object.hasOwnProperty('validateTransformed') && !object.validateTransformed(value)) { +            throw new Error('Invalid value for condition'); +        } +    } + +    return value; +} + +function conditionsNormalizeOptionValue(descriptors, type, operator, optionValue) { +    if (!descriptors.hasOwnProperty(type)) { +        throw new Error('Invalid type'); +    } + +    const conditionDescriptor = descriptors[type]; +    if (!conditionDescriptor.operators.hasOwnProperty(operator)) { +        throw new Error('Invalid operator'); +    } + +    const operatorDescriptor = conditionDescriptor.operators[operator]; + +    let transformedValue = conditionsValidateOptionValue(conditionDescriptor, optionValue); +    transformedValue = conditionsValidateOptionValue(operatorDescriptor, transformedValue); + +    if (operatorDescriptor.hasOwnProperty('transformReverse')) { +        transformedValue = operatorDescriptor.transformReverse(transformedValue); +    } +    return transformedValue; +} + +function conditionsTestValueThrowing(descriptors, type, operator, optionValue, value) { +    if (!descriptors.hasOwnProperty(type)) { +        throw new Error('Invalid type'); +    } + +    const conditionDescriptor = descriptors[type]; +    if (!conditionDescriptor.operators.hasOwnProperty(operator)) { +        throw new Error('Invalid operator'); +    } + +    const operatorDescriptor = conditionDescriptor.operators[operator]; +    if (operatorDescriptor.hasOwnProperty('transform')) { +        if (operatorDescriptor.hasOwnProperty('transformCache')) { +            const key = `${optionValue}`; +            const transformCache = operatorDescriptor.transformCache; +            if (transformCache.hasOwnProperty(key)) { +                optionValue = transformCache[key]; +            } else { +                optionValue = operatorDescriptor.transform(optionValue); +                transformCache[key] = optionValue; +            } +        } else { +            optionValue = operatorDescriptor.transform(optionValue); +        } +    } + +    return operatorDescriptor.test(value, optionValue); +} + +function conditionsTestValue(descriptors, type, operator, optionValue, value) { +    try { +        return conditionsTestValueThrowing(descriptors, type, operator, optionValue, value); +    } catch (e) { +        return false; +    } +} + +function conditionsClearCaches(descriptors) { +    for (const type in descriptors) { +        if (!descriptors.hasOwnProperty(type)) { +            continue; +        } + +        const conditionDescriptor = descriptors[type]; +        if (conditionDescriptor.hasOwnProperty('transformCache')) { +            conditionDescriptor.transformCache = {}; +        } + +        const operatorDescriptors = conditionDescriptor.operators; +        for (const operator in operatorDescriptors) { +            if (!operatorDescriptors.hasOwnProperty(operator)) { +                continue; +            } + +            const operatorDescriptor = operatorDescriptors[operator]; +            if (operatorDescriptor.hasOwnProperty('transformCache')) { +                operatorDescriptor.transformCache = {}; +            } +        } +    } +} diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js index 3dce5221..e9e321df 100644 --- a/ext/bg/js/options.js +++ b/ext/bg/js/options.js @@ -329,6 +329,22 @@ function profileOptionsUpdateVersion(options) {  /*   * Global options + * + * Each profile has an array named "conditionGroups", which is an array of condition groups + * which enable the contextual selection of profiles. The structure of the array is as follows: + * [ + *     { + *         conditions: [ + *             { + *                 type: "string", + *                 operator: "string", + *                 value: "string" + *             }, + *             // ... + *         ] + *     }, + *     // ... + * ]   */  const optionsVersionUpdates = []; @@ -351,7 +367,8 @@ function optionsUpdateVersion(options, defaultProfileOptions) {      if (profiles.length === 0) {          profiles.push({              name: 'Default', -            options: defaultProfileOptions +            options: defaultProfileOptions, +            conditionGroups: []          });      } @@ -369,6 +386,9 @@ function optionsUpdateVersion(options, defaultProfileOptions) {      // Update profile options      for (const profile of profiles) { +        if (!Array.isArray(profile.conditionGroups)) { +            profile.conditionGroups = []; +        }          profile.options = profileOptionsUpdateVersion(profile.options);      } diff --git a/ext/bg/js/profile-conditions.js b/ext/bg/js/profile-conditions.js new file mode 100644 index 00000000..86bafa95 --- /dev/null +++ b/ext/bg/js/profile-conditions.js @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019  Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * 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 <http://www.gnu.org/licenses/>. + */ + + +const 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: (value, optionValue) => (value === optionValue) +            }, +            notEqual: { +                name: '\u2260', +                test: (value, optionValue) => (value !== optionValue) +            }, +            lessThan: { +                name: '<', +                test: (value, optionValue) => (value < optionValue) +            }, +            greaterThan: { +                name: '>', +                test: (value, optionValue) => (value > optionValue) +            }, +            lessThanOrEqual: { +                name: '\u2264', +                test: (value, optionValue) => (value <= optionValue) +            }, +            greaterThanOrEqual: { +                name: '\u2265', +                test: (value, optionValue) => (value >= 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: (value, transformedOptionValue) => (transformedOptionValue.indexOf(new URL(value).hostname.toLowerCase()) >= 0) +            }, +            matchRegExp: { +                name: 'Matches RegExp', +                placeholder: 'Regular expression', +                defaultValue: 'example\\.com', +                transformCache: {}, +                transform: (optionValue) => new RegExp(optionValue, 'i'), +                transformReverse: (transformedOptionValue) => transformedOptionValue.source, +                test: (value, transformedOptionValue) => (transformedOptionValue !== null && transformedOptionValue.test(value)) +            } +        } +    } +}; diff --git a/ext/bg/js/settings-profiles.js b/ext/bg/js/settings-profiles.js index 624562c6..70f77d7b 100644 --- a/ext/bg/js/settings-profiles.js +++ b/ext/bg/js/settings-profiles.js @@ -17,6 +17,7 @@   */  let currentProfileIndex = 0; +let profileConditionsContainer = null;  function getOptionsContext() {      return { @@ -81,6 +82,19 @@ async function profileFormWrite(optionsFull) {      $('#profile-move-down').prop('disabled', currentProfileIndex >= optionsFull.profiles.length - 1);      $('#profile-name').val(profile.name); + +    if (profileConditionsContainer !== null) { +        profileConditionsContainer.cleanup(); +    } + +    profileConditionsContainer = new ConditionsUI.Container( +        profileConditionsDescriptor, +        'popupLevel', +        profile.conditionGroups, +        $('#profile-condition-groups'), +        $('#profile-add-condition-group') +    ); +    profileConditionsContainer.save = () => apiOptionsSave();  }  function profileOptionsPopulateSelect(select, profiles, currentValue, ignoreIndices) { diff --git a/ext/bg/settings.html b/ext/bg/settings.html index c0489894..d38aa090 100644 --- a/ext/bg/settings.html +++ b/ext/bg/settings.html @@ -34,6 +34,55 @@                  font-weight: normal;              } +            .form-control.is-invalid { +                border-color: #f00000; +            } + +            .condition>.condition-prefix:after { +                content: "IF"; +            } +            .condition:nth-child(n+2)>.condition-prefix:after { +                content: "AND"; +            } + +            .input-group .condition-prefix, +            .input-group .condition-group-separator-label { +                width: 60px; +                text-align: center; +            } +            .input-group .condition-group-separator-label { +                padding: 6px 12px; +                font-weight: bold; +                display: inline-block; +            } +            .input-group .condition-type, +            .input-group .condition-operator { +                width: auto; +                text-align: center; +                text-align-last: center; +            } + +            .condition-group>.condition>div:first-child { +                border-bottom-left-radius: 0; +            } +            .condition-group>.condition:nth-child(n+2)>div:first-child { +                border-top-left-radius: 0; +            } +            .condition-group>.condition:nth-child(n+2)>div:last-child>button { +                border-top-right-radius: 0; +            } +            .condition-group>.condition:nth-last-child(n+2)>div:last-child>button { +                border-bottom-right-radius: 0; +            } +            .condition-group-options>.condition-add { +                border-top-left-radius: 0; +                border-top-right-radius: 0; +            } + +            .condition-groups>*:last-of-type { +                display: none; +            } +              #custom-popup-css {                  width: 100%;                  min-height: 34px; @@ -71,7 +120,7 @@                  <h3>Profiles</h3>                  <p class="help-block"> -                    Profiles allow you to create multiple configurations and quickly switch between them. +                    Profiles allow you to create multiple configurations and quickly switch between them or use them in different contexts.                  </p>                  <div class="form-group"> @@ -100,6 +149,27 @@                      <input type="text" id="profile-name" class="form-control">                  </div> +                <div class="form-group"> +                    <label>Usage conditions</label> + +                    <p class="help-block"> +                        Usage conditions can be assigned such that certain profiles are automatically used in different contexts. +                        For example, when <a href="#popup-content-scanning">Popup Content Scanning</a> is enabled, different profiles can be used +                        depending on the level of the popup. +                    </p> + +                    <p class="help-block"> +                        Conditions are organized into groups which represent how the conditions are checked. +                        If all of the conditions in any group are met, then the profile will automatically be used for that context. +                        If no conditions are specified, the profile will only be used if it is selected as the <strong>Active profile</strong>. +                    </p> + +                    <div class="condition-groups" id="profile-condition-groups"></div> +                </div> +                <div class="form-group"> +                    <button class="btn btn-default" id="profile-add-condition-group">Add Condition Group</button> +                </div> +                  <div class="modal fade" tabindex="-1" role="dialog" id="profile-copy-modal">                      <div class="modal-dialog modal-dialog-centered">                          <div class="modal-content"> @@ -136,6 +206,20 @@                          </div>                      </div>                  </div> + +                <template id="condition-template"><div class="input-group condition"> +                    <div class="input-group-addon condition-prefix"></div> +                    <div class="input-group-btn"><select class="form-control btn btn-default condition-type"><optgroup label="Type"></optgroup></select></div> +                    <div class="input-group-btn"><select class="form-control btn btn-default condition-operator"><optgroup label="Operator"></optgroup></select></div> +                    <input type="text" class="form-control" /> +                    <div class="input-group-btn"><button class="btn btn-danger condition-remove" title="Remove"><span class="glyphicon glyphicon-remove"></span></button></div> +                </div></template> +                <template id="condition-group-separator-template"><div class="input-group"> +                    <div class="condition-group-separator-label">OR</div> +                </div></template> +                <template id="condition-group-options-template"><div class="condition-group-options"> +                    <button class="btn btn-default condition-add"><span class="glyphicon glyphicon-plus"></span></button> +                </div></template>              </div>              <div> @@ -563,9 +647,12 @@          <script src="/bg/js/anki.js"></script>          <script src="/bg/js/api.js"></script> +        <script src="/bg/js/conditions.js"></script> +        <script src="/bg/js/conditions-ui.js"></script>          <script src="/bg/js/dictionary.js"></script>          <script src="/bg/js/handlebars.js"></script>          <script src="/bg/js/options.js"></script> +        <script src="/bg/js/profile-conditions.js"></script>          <script src="/bg/js/templates.js"></script>          <script src="/bg/js/util.js"></script> |