From fe46a81cc96ec7702f629be2055ac98caae7598f Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Tue, 14 Aug 2018 11:17:47 -0400 Subject: [PATCH 1/4] export deserializeBlocks method --- src/serialization/sb3.js | 51 ++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 703640001..145ee7200 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -741,6 +741,34 @@ const deserializeFields = function (fields) { return obj; }; +/** + * Covnert serialized INPUT and FIELD primitives back to hydrated block templates. + * + * @param {object} blocks Serialized SB3 "blocks" property of a target. Will be mutated. + * @return {object} input is modified and returned + */ +const deserializeBlocks = function (blocks) { + for (const blockId in blocks) { + if (!blocks.hasOwnProperty(blockId)) { + continue; + } + const block = blocks[blockId]; + if (Array.isArray(block)) { + // this is one of the primitives + // delete the old entry in object.blocks and replace it w/the + // deserialized object + delete block[blockId]; + deserializeInputDesc(block, null, false, blocks); + continue; + } + block.id = blockId; // add id back to block since it wasn't serialized + block.inputs = deserializeInputs(block.inputs, blockId, blocks); + block.fields = deserializeFields(block.fields); + } + return blocks; +}; + + /** * Parse a single "Scratch object" and create all its in-memory VM objects. * @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher. @@ -766,25 +794,7 @@ const parseScratchObject = function (object, runtime, extensions, zip) { sprite.name = object.name; } if (object.hasOwnProperty('blocks')) { - for (const blockId in object.blocks) { - if (!object.blocks.hasOwnProperty(blockId)) continue; - const blockJSON = object.blocks[blockId]; - if (Array.isArray(blockJSON)) { - // this is one of the primitives - // delete the old entry in object.blocks and replace it w/the - // deserialized object - delete object.blocks[blockId]; - deserializeInputDesc(blockJSON, null, false, object.blocks); - continue; - } - blockJSON.id = blockId; // add id back to block since it wasn't serialized - const serializedInputs = blockJSON.inputs; - const deserializedInputs = deserializeInputs(serializedInputs, blockId, object.blocks); - blockJSON.inputs = deserializedInputs; - const serializedFields = blockJSON.fields; - const deserializedFields = deserializeFields(serializedFields); - blockJSON.fields = deserializedFields; - } + deserializeBlocks(object.blocks); // Take a second pass to create objects and add extensions for (const blockId in object.blocks) { if (!object.blocks.hasOwnProperty(blockId)) continue; @@ -1008,5 +1018,6 @@ const deserialize = function (json, runtime, zip, isSingleSprite) { module.exports = { serialize: serialize, - deserialize: deserialize + deserialize: deserialize, + deserializeBlocks: deserializeBlocks }; From df9dbd112a11e03846bf2eb99a03ce59bb7fda85 Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Wed, 15 Aug 2018 17:04:49 -0400 Subject: [PATCH 2/4] Make importing with deserialized stuff okay --- src/serialization/sb3.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 145ee7200..16ab90015 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -690,6 +690,7 @@ const deserializeInputs = function (inputs, parentId, blocks) { for (const inputName in inputs) { if (!inputs.hasOwnProperty(inputName)) continue; const inputDescArr = inputs[inputName]; + if (!Array.isArray(inputDescArr)) continue; let block = null; let shadow = null; const blockShadowInfo = inputDescArr[0]; @@ -723,6 +724,7 @@ const deserializeFields = function (fields) { for (const fieldName in fields) { if (!fields.hasOwnProperty(fieldName)) continue; const fieldDescArr = fields[fieldName]; + if (!Array.isArray(fieldDescArr)) continue; obj[fieldName] = { name: fieldName, value: fieldDescArr[0] From 896e62d2c44644dd81320bc2e4267119ba94121c Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Wed, 15 Aug 2018 17:06:30 -0400 Subject: [PATCH 3/4] Also export serializeBlocks for round-tripping --- src/serialization/sb3.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 16ab90015..b30fbda58 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -1021,5 +1021,6 @@ const deserialize = function (json, runtime, zip, isSingleSprite) { module.exports = { serialize: serialize, deserialize: deserialize, - deserializeBlocks: deserializeBlocks + deserializeBlocks: deserializeBlocks, + serializeBlocks: serializeBlocks }; From ae219e361acff324954148c50894a5193ce096fe Mon Sep 17 00:00:00 2001 From: Corey Frang Date: Mon, 20 Aug 2018 13:25:24 -0400 Subject: [PATCH 4/4] Add comments for deserialization and add tests --- src/serialization/sb3.js | 19 +++++++++++------ test/unit/serialization_sb3.js | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index b30fbda58..eaabba19d 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -18,6 +18,8 @@ const {loadCostume} = require('../import/load-costume.js'); const {loadSound} = require('../import/load-sound.js'); const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js'); +const hasOwnProperty = Object.prototype.hasOwnProperty; + /** * @typedef {object} ImportedProject * @property {Array.} targets - the imported Scratch 3.0 target objects. @@ -99,7 +101,7 @@ const primitiveOpcodeInfoMap = { const serializePrimitiveBlock = function (block) { // Returns an array represeting a primitive block or null if not one of // the primitive types above - if (primitiveOpcodeInfoMap.hasOwnProperty(block.opcode)) { + if (hasOwnProperty.call(primitiveOpcodeInfoMap, block.opcode)) { const primitiveInfo = primitiveOpcodeInfoMap[block.opcode]; const primitiveConstant = primitiveInfo[0]; const fieldName = primitiveInfo[1]; @@ -132,7 +134,7 @@ const serializePrimitiveBlock = function (block) { const serializeInputs = function (inputs) { const obj = Object.create(null); for (const inputName in inputs) { - if (!inputs.hasOwnProperty(inputName)) continue; + if (!hasOwnProperty.call(inputs, inputName)) continue; // if block and shadow refer to the same block, only serialize one if (inputs[inputName].block === inputs[inputName].shadow) { // has block and shadow, and they are the same @@ -166,7 +168,7 @@ const serializeInputs = function (inputs) { const serializeFields = function (fields) { const obj = Object.create(null); for (const fieldName in fields) { - if (!fields.hasOwnProperty(fieldName)) continue; + if (!hasOwnProperty.call(fields, fieldName)) continue; obj[fieldName] = [fields[fieldName].value]; if (fields[fieldName].hasOwnProperty('id')) { obj[fieldName].push(fields[fieldName].id); @@ -688,8 +690,9 @@ const deserializeInputs = function (inputs, parentId, blocks) { // because we call prototype functions later in the vm const obj = {}; for (const inputName in inputs) { - if (!inputs.hasOwnProperty(inputName)) continue; + if (!hasOwnProperty.call(inputs, inputName)) continue; const inputDescArr = inputs[inputName]; + // If this block has already been deserialized (it's not an array) skip it if (!Array.isArray(inputDescArr)) continue; let block = null; let shadow = null; @@ -722,8 +725,9 @@ const deserializeFields = function (fields) { // because we call prototype functions later in the vm const obj = {}; for (const fieldName in fields) { - if (!fields.hasOwnProperty(fieldName)) continue; + if (!hasOwnProperty.call(fields, fieldName)) continue; const fieldDescArr = fields[fieldName]; + // If this block has already been deserialized (it's not an array) skip it if (!Array.isArray(fieldDescArr)) continue; obj[fieldName] = { name: fieldName, @@ -745,13 +749,16 @@ const deserializeFields = function (fields) { /** * Covnert serialized INPUT and FIELD primitives back to hydrated block templates. + * Should be able to deserialize a format that has already been deserialized. The only + * "east" path to adding new targets/code requires going through deserialize, so it should + * work with pre-parsed deserialized blocks. * * @param {object} blocks Serialized SB3 "blocks" property of a target. Will be mutated. * @return {object} input is modified and returned */ const deserializeBlocks = function (blocks) { for (const blockId in blocks) { - if (!blocks.hasOwnProperty(blockId)) { + if (!Object.prototype.hasOwnProperty.call(blocks, blockId)) { continue; } const block = blocks[blockId]; diff --git a/test/unit/serialization_sb3.js b/test/unit/serialization_sb3.js index 40cc8e23b..185cb87aa 100644 --- a/test/unit/serialization_sb3.js +++ b/test/unit/serialization_sb3.js @@ -182,3 +182,42 @@ test('serialize sb3 preserves sprite layer order', t => { t.end(); }); }); + +test('serializeBlocks', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB3ProjectPath)) + .then(() => { + const blocks = vm.runtime.targets[1].blocks._blocks; + const result = sb3.serializeBlocks(blocks); + // @todo Analyze + t.type(result[0], 'object'); + t.ok(Object.keys(result[0]).length < Object.keys(blocks).length, 'less blocks in serialized format'); + t.ok(Array.isArray(result[1])); + t.end(); + }); +}); + +test('deserializeBlocks', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB3ProjectPath)) + .then(() => { + const blocks = vm.runtime.targets[1].blocks._blocks; + const serialized = sb3.serializeBlocks(blocks)[0]; + const deserialized = sb3.deserializeBlocks(serialized); + t.equal(Object.keys(deserialized).length, Object.keys(blocks).length, 'same number of blocks'); + t.end(); + }); +}); + +test('deserializeBlocks on already deserialized input', t => { + const vm = new VirtualMachine(); + vm.loadProject(readFileToBuffer(commentsSB3ProjectPath)) + .then(() => { + const blocks = vm.runtime.targets[1].blocks._blocks; + const serialized = sb3.serializeBlocks(blocks)[0]; + const deserialized = sb3.deserializeBlocks(serialized); + const deserializedAgain = sb3.deserializeBlocks(deserialized); + t.deepEqual(deserialized, deserializedAgain, 'no change from second pass of deserialize'); + t.end(); + }); +});