Spaces:
Running
Running
File size: 8,183 Bytes
30c32c8 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
const Color = require('../util/color');
/**
* @fileoverview
* Utilities for casting and comparing Scratch data-types.
* Scratch behaves slightly differently from JavaScript in many respects,
* and these differences should be encapsulated below.
* For example, in Scratch, add(1, join("hello", world")) -> 1.
* This is because "hello world" is cast to 0.
* In JavaScript, 1 + Number("hello" + "world") would give you NaN.
* Use when coercing a value before computation.
*/
/**
* Used internally by compare()
* @param {*} val A value that evaluates to 0 in JS string-to-number conversation such as empty string, 0, or tab.
* @returns {boolean} True if the value should not be treated as the number zero.
*/
const isNotActuallyZero = val => {
if (typeof val !== 'string') return false;
for (let i = 0; i < val.length; i++) {
const code = val.charCodeAt(i);
// '0'.charCodeAt(0) === 48
// '\t'.charCodeAt(0) === 9
// We include tab for compatibility with scratch-www's broken trim() polyfill.
// https://github.com/TurboWarp/scratch-vm/issues/115
// https://scratch.mit.edu/projects/788261699/
if (code === 48 || code === 9) {
return false;
}
}
return true;
};
class Cast {
/**
* Scratch cast to number.
* Treats NaN as 0.
* In Scratch 2.0, this is captured by `interp.numArg.`
* @param {*} value Value to cast to number.
* @return {number} The Scratch-casted number value.
*/
static toNumber (value) {
// If value is already a number we don't need to coerce it with
// Number().
if (typeof value === 'number') {
// Scratch treats NaN as 0, when needed as a number.
// E.g., 0 + NaN -> 0.
if (Number.isNaN(value)) {
return 0;
}
return value;
}
const n = Number(value);
if (Number.isNaN(n)) {
// Scratch treats NaN as 0, when needed as a number.
// E.g., 0 + NaN -> 0.
return 0;
}
return n;
}
/**
* Scratch cast to boolean.
* In Scratch 2.0, this is captured by `interp.boolArg.`
* Treats some string values differently from JavaScript.
* @param {*} value Value to cast to boolean.
* @return {boolean} The Scratch-casted boolean value.
*/
static toBoolean (value) {
// Already a boolean?
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
// These specific strings are treated as false in Scratch.
if ((value === '') ||
(value === '0') ||
(value.toLowerCase() === 'false')) {
return false;
}
// All other strings treated as true.
return true;
}
// Coerce other values and numbers.
return Boolean(value);
}
/**
* Scratch cast to string.
* @param {*} value Value to cast to string.
* @return {string} The Scratch-casted string value.
*/
static toString (value) {
return String(value);
}
/**
* Cast any Scratch argument to an RGB color array to be used for the renderer.
* @param {*} value Value to convert to RGB color array.
* @return {Array.<number>} [r,g,b], values between 0-255.
*/
static toRgbColorList (value) {
const color = Cast.toRgbColorObject(value);
return [color.r, color.g, color.b];
}
/**
* Cast any Scratch argument to an RGB color object to be used for the renderer.
* @param {*} value Value to convert to RGB color object.
* @return {RGBOject} [r,g,b], values between 0-255.
*/
static toRgbColorObject (value) {
let color;
if (typeof value === 'string' && value.substring(0, 1) === '#') {
color = Color.hexToRgb(value);
// If the color wasn't *actually* a hex color, cast to black
if (!color) color = {r: 0, g: 0, b: 0, a: 255};
} else {
color = Color.decimalToRgb(Cast.toNumber(value));
}
return color;
}
/**
* Determine if a Scratch argument is a white space string (or null / empty).
* @param {*} val value to check.
* @return {boolean} True if the argument is all white spaces or null / empty.
*/
static isWhiteSpace (val) {
return val === null || (typeof val === 'string' && val.trim().length === 0);
}
/**
* Compare two values, using Scratch cast, case-insensitive string compare, etc.
* In Scratch 2.0, this is captured by `interp.compare.`
* @param {*} v1 First value to compare.
* @param {*} v2 Second value to compare.
* @returns {number} Negative number if v1 < v2; 0 if equal; positive otherwise.
*/
static compare (v1, v2) {
let n1 = Number(v1);
let n2 = Number(v2);
if (n1 === 0 && isNotActuallyZero(v1)) {
n1 = NaN;
} else if (n2 === 0 && isNotActuallyZero(v2)) {
n2 = NaN;
}
if (isNaN(n1) || isNaN(n2)) {
// At least one argument can't be converted to a number.
// Scratch compares strings as case insensitive.
const s1 = String(v1).toLowerCase();
const s2 = String(v2).toLowerCase();
if (s1 < s2) {
return -1;
} else if (s1 > s2) {
return 1;
}
return 0;
}
// Handle the special case of Infinity
if (
(n1 === Infinity && n2 === Infinity) ||
(n1 === -Infinity && n2 === -Infinity)
) {
return 0;
}
// Compare as numbers.
return n1 - n2;
}
/**
* Determine if a Scratch argument number represents a round integer.
* @param {*} val Value to check.
* @return {boolean} True if number looks like an integer.
*/
static isInt (val) {
// Values that are already numbers.
if (typeof val === 'number') {
if (isNaN(val)) { // NaN is considered an integer.
return true;
}
// True if it's "round" (e.g., 2.0 and 2).
return val === Math.floor(val);
} else if (typeof val === 'boolean') {
// `True` and `false` always represent integer after Scratch cast.
return true;
} else if (typeof val === 'string') {
// If it contains a decimal point, don't consider it an int.
return val.indexOf('.') < 0;
}
return false;
}
static get LIST_INVALID () {
return 'INVALID';
}
static get LIST_ALL () {
return 'ALL';
}
/**
* Compute a 1-based index into a list, based on a Scratch argument.
* Two special cases may be returned:
* LIST_ALL: if the block is referring to all of the items in the list.
* LIST_INVALID: if the index was invalid in any way.
* @param {*} index Scratch arg, including 1-based numbers or special cases.
* @param {number} length Length of the list.
* @param {boolean} acceptAll Whether it should accept "all" or not.
* @return {(number|string)} 1-based index for list, LIST_ALL, or LIST_INVALID.
*/
static toListIndex (index, length, acceptAll) {
if (typeof index !== 'number') {
if (index === 'all') {
return acceptAll ? Cast.LIST_ALL : Cast.LIST_INVALID;
}
if (index === 'last') {
if (length > 0) {
return length;
}
return Cast.LIST_INVALID;
} else if (index === 'random' || index === 'any') {
if (length > 0) {
return 1 + Math.floor(Math.random() * length);
}
return Cast.LIST_INVALID;
}
}
index = Math.floor(Cast.toNumber(index));
if (index < 1 || index > length) {
return Cast.LIST_INVALID;
}
return index;
}
}
module.exports = Cast;
|