summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json1
-rw-r--r--ext/mixed/js/core.js105
-rw-r--r--package.json2
-rw-r--r--test/test-core.js169
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(); }