diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index df367cf1b..ef5010a90 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -14,6 +14,7 @@ const StageLayering = require('../engine/stage-layering'); const log = require('../util/log'); const uid = require('../util/uid'); const MathUtil = require('../util/math-util'); +const StringUtil = require('../util/string-util'); const {loadCostume} = require('../import/load-costume.js'); const {loadSound} = require('../import/load-sound.js'); @@ -522,7 +523,7 @@ const serialize = function (runtime, targetId) { const layerOrdering = getSimplifiedLayerOrdering(originalTargetsToSerialize); - const flattenedOriginalTargets = JSON.parse(JSON.stringify(originalTargetsToSerialize)); + const flattenedOriginalTargets = JSON.parse(StringUtil.stringify(originalTargetsToSerialize)); // If the renderer is attached, and we're serializing a whole project (not a sprite) // add a temporary layerOrder property to each target. diff --git a/src/util/string-util.js b/src/util/string-util.js index 1d8f18d4f..c308cc6c8 100644 --- a/src/util/string-util.js +++ b/src/util/string-util.js @@ -34,7 +34,28 @@ class StringUtil { return [text.substring(0, index), text.substring(index + 1)]; } return [text, null]; - + + } + + /** + * A customized version of JSON.stringify that sets Infinity/NaN to 0, + * instead of the default (null). + * Needed because null is not of type number, but Infinity/NaN are, which + * can lead to serialization producing JSON that isn't valid based on the parser schema. + * It is also consistent with the behavior of saving 2.0 projects. + * This is only needed when stringifying an object for saving. + * + * @param {!object} obj - The object to serialize + * @return {!string} The JSON.stringified string with Infinity/NaN replaced with 0 + */ + static stringify (obj) { + return JSON.stringify(obj, (_key, value) => { + if (typeof value === 'number' && + (value === Infinity || value === -Infinity || isNaN(value))){ + return 0; + } + return value; + }); } } diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 2bc8eb0d4..e25f757a1 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -381,7 +381,7 @@ class VirtualMachine extends EventEmitter { exportSprite (targetId, optZipType) { const soundDescs = serializeSounds(this.runtime, targetId); const costumeDescs = serializeCostumes(this.runtime, targetId); - const spriteJson = JSON.stringify(sb3.serialize(this.runtime, targetId)); + const spriteJson = StringUtil.stringify(sb3.serialize(this.runtime, targetId)); const zip = new JSZip(); zip.file('sprite.json', spriteJson); @@ -401,7 +401,7 @@ class VirtualMachine extends EventEmitter { * @return {string} Serialized state of the runtime. */ toJSON () { - return JSON.stringify(sb3.serialize(this.runtime)); + return StringUtil.stringify(sb3.serialize(this.runtime)); } // TODO do we still need this function? Keeping it here so as not to introduce diff --git a/test/unit/util_string.js b/test/unit/util_string.js index aca20f9a4..dd2fb2339 100644 --- a/test/unit/util_string.js +++ b/test/unit/util_string.js @@ -63,3 +63,14 @@ test('unusedName', t => { ); t.end(); }); + +test('stringify', t => { + const obj = {a: Infinity, b: NaN, c: -Infinity, d: 23, e: 'str'}; + const parsed = JSON.parse(StringUtil.stringify(obj)); + t.equal(parsed.a, 0); + t.equal(parsed.b, 0); + t.equal(parsed.c, 0); + t.equal(parsed.d, 23); + t.equal(parsed.e, 'str'); + t.end(); +}); diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js index 78036fdcf..6bac07f17 100644 --- a/test/unit/virtual-machine.js +++ b/test/unit/virtual-machine.js @@ -982,3 +982,30 @@ test('Starting the VM emits an event', t => { t.equal(started, true); t.end(); }); + +test('toJSON encodes Infinity/NaN as 0, not null', t => { + const vm = new VirtualMachine(); + const runtime = vm.runtime; + const spr1 = new Sprite(null, runtime); + const stage = spr1.createClone(); + stage.isStage = true; + stage.volume = Infinity; + stage.tempo = NaN; + stage.createVariable('id1', 'name1', ''); + stage.variables.id1.value = Infinity; + stage.createVariable('id2', 'name2', ''); + stage.variables.id1.value = -Infinity; + stage.createVariable('id3', 'name3', ''); + stage.variables.id1.value = NaN; + + runtime.targets = [stage]; + + const json = JSON.parse(vm.toJSON()); + t.equal(json.targets[0].volume, 0); + t.equal(json.targets[0].tempo, 0); + t.equal(json.targets[0].variables.id1[1], 0); + t.equal(json.targets[0].variables.id2[1], 0); + t.equal(json.targets[0].variables.id3[1], 0); + + t.end(); +});