diff options
| -rw-r--r-- | .eslintrc.json | 1 | ||||
| -rw-r--r-- | ext/mixed/js/core.js | 105 | ||||
| -rw-r--r-- | package.json | 2 | ||||
| -rw-r--r-- | test/test-core.js | 169 | 
4 files changed, 275 insertions, 2 deletions
| diff --git a/.eslintrc.json b/.eslintrc.json index 2ff44ead..663e9003 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -108,6 +108,7 @@                  "deferPromise": "readonly",                  "clone": "readonly",                  "generateId": "readonly", +                "DynamicProperty": "readonly",                  "EventDispatcher": "readonly",                  "EventListenerCollection": "readonly"              } diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js index 9142a846..5bee4670 100644 --- a/ext/mixed/js/core.js +++ b/ext/mixed/js/core.js @@ -260,7 +260,7 @@ function promiseTimeout(delay, resolveValue) {  /* - * Common events + * Common classes   */  class EventDispatcher { @@ -348,3 +348,106 @@ class EventListenerCollection {          this._eventListeners = [];      }  } + +/** + * Class representing a generic value with an override stack. + * Changes can be observed by listening to the 'change' event. + */ +class DynamicProperty extends EventDispatcher { +    /** +     * Creates a new instance with the specified value. +     * @param value The value to assign. +     */ +    constructor(value) { +        super(); +        this._value = value; +        this._defaultValue = value; +        this._overrides = []; +    } + +    /** +     * Gets the default value for the property, which is assigned to the +     * public value property when no overrides are present. +     */ +    get defaultValue() { +        return this._defaultValue; +    } + +    /** +     * Assigns the default value for the property. If no overrides are present +     * and if the value is different than the current default value, +     * the 'change' event will be triggered. +     * @param value The value to assign. +     */ +    set defaultValue(value) { +        this._defaultValue = value; +        if (this._overrides.length === 0) { this._updateValue(); } +    } + +    /** +     * Gets the current value for the property, taking any overrides into account. +     */ +    get value() { +        return this._value; +    } + +    /** +     * Gets the number of overrides added to the property. +     */ +    get overrideCount() { +        return this._overrides.length; +    } + +    /** +     * Adds an override value with the specified priority to the override stack. +     * Values with higher priority will take precedence over those with lower. +     * For tie breaks, the override value added first will take precedence. +     * If the newly added override has the highest priority of all overrides +     * and if the override value is different from the current value, +     * the 'change' event will be fired. +     * @param value The override value to assign. +     * @param priority The priority value to use, as a number. +     * @returns A string token which can be passed to the clearOverride function +     *  to remove the override. +     */ +    setOverride(value, priority=0) { +        const overridesCount = this._overrides.length; +        let i = 0; +        for (; i < overridesCount; ++i) { +            if (priority > this._overrides[i].priority) { break; } +        } +        const token = generateId(16); +        this._overrides.splice(i, 0, {value, priority, token}); +        if (i === 0) { this._updateValue(); } +        return token; +    } + +    /** +     * Removes a specific override value. If the removed override +     * had the highest priority, and the new value is different from +     * the previous value, the 'change' event will be fired. +     * @param token The token for the corresponding override which is to be removed. +     * @returns true if an override was returned, false otherwise. +     */ +    clearOverride(token) { +        for (let i = 0, ii = this._overrides.length; i < ii; ++i) { +            if (this._overrides[i].token === token) { +                this._overrides.splice(i, 1); +                if (i === 0) { this._updateValue(); } +                return true; +            } +        } +        return false; +    } + +    /** +     * Updates the current value using the current overrides and default value. +     * If the new value differs from the previous value, the 'change' event will be fired. +     */ +    _updateValue() { +        const value = this._overrides.length > 0 ? this._overrides[0].value : this._defaultValue; +        if (this._value === value) { return; } +        this._value = value; +        this.trigger('change', {value}); +    } +} diff --git a/package.json b/package.json index 8bbf883a..d276d701 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@          "build": "node ./dev/build.js",          "test": "npm run test-lint && npm run test-code && npm run test-manifest",          "test-lint": "eslint . && node ./test/lint/global-declarations.js", -        "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document-util.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js && node ./test/test-dom-text-scanner.js && node ./test/test-cache-map.js && node ./test/test-profile-conditions.js", +        "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document-util.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js && node ./test/test-dom-text-scanner.js && node ./test/test-cache-map.js && node ./test/test-profile-conditions.js && node ./test/test-core.js",          "test-manifest": "node ./test/test-manifest.js"      },      "repository": { diff --git a/test/test-core.js b/test/test-core.js new file mode 100644 index 00000000..d4a880d1 --- /dev/null +++ b/test/test-core.js @@ -0,0 +1,169 @@ +/* + * Copyright (C) 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 <https://www.gnu.org/licenses/>. + */ + +const assert = require('assert'); +const crypto = require('crypto'); +const {VM} = require('./yomichan-vm'); + +const vm = new VM({ +    crypto: { +        getRandomValues: (array) => { +            const buffer = crypto.randomBytes(array.byteLength); +            buffer.copy(array); +            return array; +        } +    } +}); +vm.execute([ +    'mixed/js/core.js' +]); +const [DynamicProperty] = vm.get(['DynamicProperty']); + + +function testDynamicProperty() { +    const data = [ +        { +            initialValue: 0, +            operations: [ +                { +                    operation: null, +                    expectedDefaultValue: 0, +                    expectedValue: 0, +                    expectedOverrideCount: 0, +                    expeectedEventOccurred: false +                }, +                { +                    operation: 'set.defaultValue', +                    args: [1], +                    expectedDefaultValue: 1, +                    expectedValue: 1, +                    expectedOverrideCount: 0, +                    expeectedEventOccurred: true +                }, +                { +                    operation: 'set.defaultValue', +                    args: [1], +                    expectedDefaultValue: 1, +                    expectedValue: 1, +                    expectedOverrideCount: 0, +                    expeectedEventOccurred: false +                }, +                { +                    operation: 'set.defaultValue', +                    args: [0], +                    expectedDefaultValue: 0, +                    expectedValue: 0, +                    expectedOverrideCount: 0, +                    expeectedEventOccurred: true +                }, +                { +                    operation: 'setOverride', +                    args: [8], +                    expectedDefaultValue: 0, +                    expectedValue: 8, +                    expectedOverrideCount: 1, +                    expeectedEventOccurred: true +                }, +                { +                    operation: 'setOverride', +                    args: [16], +                    expectedDefaultValue: 0, +                    expectedValue: 8, +                    expectedOverrideCount: 2, +                    expeectedEventOccurred: false +                }, +                { +                    operation: 'setOverride', +                    args: [32, 1], +                    expectedDefaultValue: 0, +                    expectedValue: 32, +                    expectedOverrideCount: 3, +                    expeectedEventOccurred: true +                }, +                { +                    operation: 'setOverride', +                    args: [64, -1], +                    expectedDefaultValue: 0, +                    expectedValue: 32, +                    expectedOverrideCount: 4, +                    expeectedEventOccurred: false +                }, +                { +                    operation: 'clearOverride', +                    args: [-4], +                    expectedDefaultValue: 0, +                    expectedValue: 32, +                    expectedOverrideCount: 3, +                    expeectedEventOccurred: false +                }, +                { +                    operation: 'clearOverride', +                    args: [-3], +                    expectedDefaultValue: 0, +                    expectedValue: 32, +                    expectedOverrideCount: 2, +                    expeectedEventOccurred: false +                }, +                { +                    operation: 'clearOverride', +                    args: [-2], +                    expectedDefaultValue: 0, +                    expectedValue: 64, +                    expectedOverrideCount: 1, +                    expeectedEventOccurred: true +                }, +                { +                    operation: 'clearOverride', +                    args: [-1], +                    expectedDefaultValue: 0, +                    expectedValue: 0, +                    expectedOverrideCount: 0, +                    expeectedEventOccurred: true +                } +            ] +        } +    ]; + +    for (const {initialValue, operations} of data) { +        const property = new DynamicProperty(initialValue); +        const overrideTokens = []; +        let eventOccurred = false; +        const onChange = () => { eventOccurred = true; }; +        property.on('change', onChange); +        for (const {operation, args, expectedDefaultValue, expectedValue, expectedOverrideCount, expeectedEventOccurred} of operations) { +            eventOccurred = false; +            switch (operation) { +                case 'set.defaultValue': property.defaultValue = args[0]; break; +                case 'setOverride': overrideTokens.push(property.setOverride(...args)); break; +                case 'clearOverride': property.clearOverride(overrideTokens[overrideTokens.length + args[0]]); break; +            } +            assert.strictEqual(eventOccurred, expeectedEventOccurred); +            assert.strictEqual(property.defaultValue, expectedDefaultValue); +            assert.strictEqual(property.value, expectedValue); +            assert.strictEqual(property.overrideCount, expectedOverrideCount); +        } +        property.off('change', onChange); +    } +} + + +function main() { +    testDynamicProperty(); +} + + +if (require.main === module) { main(); } |