Read about our upcoming Code of Conduct on this issue

Commit 39d14a15 authored by Leon Arnott's avatar Leon Arnott
Browse files

Altered "is", "is in" and "contains" to use compare-by-value.

Now, instead of using JS's compare-by-reference semantics, TwineScript compares containers by value - that is, by checking if their contents are identical. This brings them into alignment with the copy-by-value semantics used by (set:) and such.
parent 939408328acd
define(['utils', 'macros'], function(Utils, Macros) {
define(['utils', 'macros', 'utils/operationutils'], function(Utils, Macros, OperationUtils) {
"use strict";
/*
A ChangerCommand is an author-facing function that is used to mutate a ChangeDescriptor
object, that itself is used to alter a Section's rendering.
A ChangerCommand is a command that is used to alter the way a particular
Section renders the value. It does this by mutating a passed-in ChangeDescriptor
object in some way.
This decorator function accepts a function (which defines the ChangerCommand's
internal implementation), the name of the macro that created it, and some
author-supplied configuration parameters, and creates a partial
function augmented with the necessary TwineScript related methods.
For instance, for (transition: "dissolve"), the name would be
"transition" and params would be ["dissolve"].
Since it basically transforms an existing function without modifying its prototype,
it isn't really a "class", and thus isn't a prototype object with a .create() method.
ChangerCommands are first-class values so that they can be saved and combined
by the author to create "custom styles" for sections of story text.
*/
var ChangerCommand = {
......@@ -30,6 +23,17 @@ define(['utils', 'macros'], function(Utils, Macros) {
return "[A '" + this.macroName + "' command]";
},
/*
ChangerCommands are created and returned changer macro calls.
The arguments passed to them are essentially direct representations
of the macro call itself.
For instance, (font: "Skia") would result in a call of
ChangerCommand.create("font", ["Skia"])
@param {String} macroName
@param {Array} params
@param {ChangerCommand} next
*/
create: function(macroName, params, next) {
Utils.assert(params === undefined || Array.isArray(params));
......@@ -59,6 +63,14 @@ define(['utils', 'macros'], function(Utils, Macros) {
return ret;
},
"TwineScript_is": function(other) {
if (ChangerCommand.isPrototypeOf(other)) {
return this.macroName === other.macroName &&
OperationUtils.is(this.params, other.params) &&
OperationUtils.is(this.next, other.next);
}
},
TwineScript_Clone: function() {
return this.create(this.macroName, this.params, this.next);
},
......
......@@ -98,12 +98,15 @@ define(['utils', 'jquery'], function(Utils, $){
+ [this.r, this.g, this.b].join(',') + ");'></span>";
},
TwineScript_Clone: function() {
return Colour.create(this);
TwineScript_is: function(other) {
return Colour.isPrototypeOf(other) &&
other.r === this.r &&
other.g === this.g &&
other.b === this.b;
},
toJSON: function() {
return Object.assign({ colour: true }, this);
TwineScript_Clone: function() {
return Colour.create(this);
},
/*
......
......@@ -111,6 +111,9 @@ define(['utils/hookutils', 'jquery'],function(HookUtils, $) {
in an AssignmentRequest. Yes, (set: $grault to ?garply) only
copies ?garply's present value into $grault, as if ?garply
were a variable.
Note that as a result, it's not possible to store
a HookSet in a variable - which is good, as it carries with it
a reference to its origin hook.
*/
TwineScript_AssignValue: function() {
return jQueryCall.call(this, "text");
......
......@@ -100,6 +100,21 @@ function(Utils, State, Story, Colour, AssignmentRequest, OperationUtils) {
+ objectName(obj) + ".");
}
}
/*
Sets, being essentially a limited kind of arrays, cannot have any
property access other than 'length'.
*/
else if (obj instanceof Set) {
if (prop !== "length") {
return new Error("You can only get the 'length' of a " + objectName(obj)
+ ". To check contained values, use the 'contains' operator.");
}
/*
This kludge must be used to pave over a little difference
between Arrays and Sets.
*/
prop = "size";
}
return prop;
}
......@@ -326,7 +341,8 @@ function(Utils, State, Story, Colour, AssignmentRequest, OperationUtils) {
"<=": comparisonOp( onlyNumbers( doNotCoerce(function(l,r) { return l <= r; }), "do <= to")),
">=": comparisonOp( onlyNumbers( doNotCoerce(function(l,r) { return l >= r; }), "do >= to")),
is: comparisonOp(Object.is),
is: comparisonOp(OperationUtils.is),
isNot: comparisonOp(function(l,r) {
return !Operations.is(l,r);
}),
......
......@@ -182,11 +182,52 @@ define(['utils'], function(Utils) {
);
}
/*
As TwineScript uses pass-by-value rather than pass-by-reference
for all objects, it must also use compare-by-value for objects as well.
This function implements the "is" operation.
@return {Boolean}
*/
function is(l, r) {
/*
For primitives, === is sufficient.
*/
if (typeof l !== "object" && typeof r !== "object") {
return l === r;
}
/*
For Arrays, compare every element and position of one
with the other.
*/
if (Array.isArray(l) && Array.isArray(r)) {
if (l.length < r.length) {
return is(r, l);
}
return l.every(function(element, index) {
return is(r[index], element);
});
}
/*
For Maps and Sets, simply reduce them to Arrays.
*/
if (l instanceof Map && r instanceof Map) {
// Don't forget that Map.prototype.entries() returns an iterator!
return is(Array.from(l.entries()), Array.from(r.entries()));
}
if (l instanceof Set && r instanceof Set) {
return is(Array.from(l.values()), Array.from(r.values()));
}
if (l && typeof l.TwineScript_is === "function") {
return l.TwineScript_is(r);
}
return Object.is(l, r);
}
/*
As the base function for Operations.contains,
this implements the "x contains y" and "y is in x" keywords.
This is placed outside so that Operation.isIn can call it.
@return {String}
@return {Boolean}
*/
function contains(container,obj) {
/*
......@@ -195,23 +236,27 @@ define(['utils'], function(Utils) {
compare-by-value proposition.
*/
if (container) {
/*
Basic array or string indexOf check.
*/
if (isSequential(container)) {
if (typeof container === "string") {
return container.indexOf(obj) > -1;
}
if(Array.isArray(container)) {
return container.some(function(e) {
return is(e, obj);
});
}
/*
For Sets and Maps, use .has().
For Sets and Maps, check that the key exists.
*/
if (container instanceof Set || container instanceof Map) {
return container.has(obj);
return Array.from(container.keys()).some(function(e) {
return is(e, obj);
});
}
}
/*
Default: since "'r' is in 'r'" is true, so is "false is in false".
*/
return Object.is(container, obj);
return is(container, obj);
}
var OperationUtils = Object.freeze({
......@@ -222,6 +267,7 @@ define(['utils'], function(Utils) {
coerceToString: coerceToString,
objectName: objectName,
typeName: typeName,
is: is,
contains: contains,
});
return OperationUtils;
......
......@@ -7,3 +7,4 @@
{(dataset: "Sword", "Axe", "Knife")
(set: $a to (dataset: "Sword", "Axe", "Knife"))
(print: $a contains "Sword")} (= true)
(print: $a's length) (= 3)
<!DOCTYPE html><meta charset="utf-8">
<title>Is Test</title>
<link rel="stylesheet" href="../build/harlowe-css.css"/>
<tw-story></tw-story><script data-main="../js/harlowe.js" src="../node_modules/requirejs/require.js"></script>
<tw-storydata options="debug" startnode="1">
<tw-passagedata pid="1" name="1" position="512,384" data-t8n="instant">
(print: (a:) is (a:)) (= true)
(print: (a:1,2,3,4,5) is (range:1,5)) (= true)
(set: $a to (a:1,2))(print: $a is (a:1,2)) (= true)
(print: (a: $a) is (a:$a)) (= true)
(print: (datamap:"Eggs",2) is (datamap:"Eggs",2)) (= true)
(print: (datamap:"Eggs",2) is (datamap:"Eggs",3)) (= false)
(print: (datamap:"Eggs",2) is (datamap:"Egg",2)) (= false)
(print: (dataset:"Eggs") is (dataset:"Eggs")) (= true)
(print: (dataset:"Eggs") is (dataset:"Eggs",2)) (= false)
(print: (font:"Skia") is (font:"Skia")) (= true)
(print: (font:"Skia") + (text-style:"bold") is (font:"Skia") + (text-style:"bold")) (= true)
......@@ -14,3 +14,8 @@
(set: $a's 1st to 1)
(print: 1 is in $a) (= true)
(print: $a contains 1) (= true)
(print: (a:) is in (a:(a:))) (= true)
(print: (dataset:) is in (a:(dataset:))) (= true)
(print: (dataset:2,3) is in (dataset:4,(dataset:2,3))) (= true)
(print: (dataset:(a:"Red")) contains "e") (= false)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment