diff --git a/core/serialization/blocks.js b/core/serialization/blocks.js index 1ad9b57b439..4ab79a3a45b 100644 --- a/core/serialization/blocks.js +++ b/core/serialization/blocks.js @@ -15,11 +15,25 @@ goog.module.declareLegacyNamespace(); // eslint-disable-next-line no-unused-vars const Block = goog.requireType('Blockly.Block'); +// eslint-disable-next-line no-unused-vars +const Connection = goog.requireType('Blockly.Connection'); +const Xml = goog.require('Blockly.Xml'); +const inputTypes = goog.require('Blockly.inputTypes'); // TODO: Remove this once lint is fixed. /* eslint-disable no-use-before-define */ +/** + * Represents the state of a connection. + * @typedef {{ + * shadow: (!State|undefined), + * block: (!State|undefined) + * }} + */ +var ConnectionState; +exports.ConnectionState = ConnectionState; + /** * Represents the state of a given block. * @typedef {{ @@ -35,7 +49,9 @@ const Block = goog.requireType('Blockly.Block'); * inline: (boolean|undefined), * data: (string|undefined), * extra-state: *, - * fields: (Object|undefined), + * fields: (!Object|undefined), + * inputs: (!Object|undefined), + * next: (!ConnectionState|undefined) * }} */ var State; @@ -44,14 +60,27 @@ exports.State = State; /** * Returns the state of the given block as a plain JavaScript object. * @param {!Block} block The block to serialize. - * @param {{addCoordinates: (boolean|undefined)}=} param1 - * addCoordinates: If true the coordinates of the block are added to the + * @param {{addCoordinates: (boolean|undefined), addInputBlocks: + * (boolean|undefined), addNextBlocks: (boolean|undefined)}=} param1 + * addCoordinates: If true, the coordinates of the block are added to the * serialized state. False by default. + * addinputBlocks: If true, children of the block which are connected to + * inputs will be serialized. True by default. + * addNextBlocks: If true, children of the block which are connected to the + * block's next connection (if it exists) will be serialized. + * True by default. * @return {?State} The serialized state of the * block, or null if the block could not be serialied (eg it was an * insertion marker). */ -const save = function(block, {addCoordinates = false} = {}) { +const save = function( + block, + { + addCoordinates = false, + addInputBlocks = true, + addNextBlocks = true + } = {} +) { if (block.isInsertionMarker()) { return null; } @@ -62,11 +91,17 @@ const save = function(block, {addCoordinates = false} = {}) { }; if (addCoordinates) { - addCoords(block, state); + saveCoords(block, state); + } + saveAttributes(block, state); + saveExtraState(block, state); + saveFields(block, state); + if (addInputBlocks) { + saveInputBlocks(block, state); + } + if (addNextBlocks) { + saveNextBlocks(block, state); } - addAttributes(block, state); - addExtraState(block, state); - addFields(block, state); return state; }; @@ -78,7 +113,7 @@ exports.save = save; * @param {!Block} block The block to base the attributes on. * @param {!State} state The state object to append to. */ -const addAttributes = function(block, state) { +const saveAttributes = function(block, state) { if (block.isCollapsed()) { state['collapsed'] = true; } @@ -111,7 +146,7 @@ const addAttributes = function(block, state) { * @param {!Block} block The block to base the coordinates on * @param {!State} state The state object to append to */ -const addCoords = function(block, state) { +const saveCoords = function(block, state) { const workspace = block.workspace; const xy = block.getRelativeToSurfaceXY(); state['x'] = Math.round(workspace.RTL ? workspace.getWidth() - xy.x : xy.x); @@ -123,7 +158,7 @@ const addCoords = function(block, state) { * @param {!Block} block The block to serialize the extra state of. * @param {!State} state The state object to append to. */ -const addExtraState = function(block, state) { +const saveExtraState = function(block, state) { if (block.saveExtraState) { const extraState = block.saveExtraState(); if (extraState !== null) { @@ -137,7 +172,7 @@ const addExtraState = function(block, state) { * @param {!Block} block The block to serialize the field state of. * @param {!State} state The state object to append to. */ -const addFields = function(block, state) { +const saveFields = function(block, state) { let hasFieldState = false; let fields = Object.create(null); for (let i = 0; i < block.inputList.length; i++) { @@ -154,3 +189,69 @@ const addFields = function(block, state) { state['fields'] = fields; } }; + +/** + * Adds the state of all of the child blocks of the given block (which are + * connected to inputs) to the given state object. + * @param {!Block} block The block to serialize the input blocks of. + * @param {!State} state The state object to append to. + */ +const saveInputBlocks = function(block, state) { + const inputs = Object.create(null); + for (let i = 0; i < block.inputList.length; i++) { + const input = block.inputList[i]; + if (input.type === inputTypes.DUMMY) { + continue; + } + const connectionState = + saveConnection(/** @type {!Connection} */ (input.connection)); + if (connectionState) { + inputs[input.name] = connectionState; + } + } + + if (Object.keys(inputs).length) { + state['inputs'] = inputs; + } +}; + +/** + * Adds the state of all of the next blocks of the given block to the given + * state object. + * @param {!Block} block The block to serialize the next blocks of. + * @param {!State} state The state object to append to. + */ +const saveNextBlocks = function(block, state) { + if (!block.nextConnection) { + return; + } + const connectionState = saveConnection(block.nextConnection); + if (connectionState) { + state['next'] = connectionState; + } +}; + +/** + * Returns the state of the given connection (ie the state of any connected + * shadow or real blocks). + * @param {!Connection} connection The connection to serialize the connected + * blocks of. + * @return {?ConnectionState} An object containing the state of any connected + * shadow block, or any connected real block. + */ +const saveConnection = function(connection) { + const shadow = connection.getShadowDom(); + const child = connection.targetBlock(); + if (!shadow && !child) { + return null; + } + var state = Object.create(null); + if (shadow) { + state['shadow'] = Xml.domToText(shadow) + .replace('xmlns="https://developers.google.com/blockly/xml"', ''); + } + if (child) { + state['block'] = save(child); + } + return state; +}; diff --git a/tests/deps.js b/tests/deps.js index bf85399f873..43cdc5eb452 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -196,7 +196,7 @@ goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Ren goog.addDependency('../../core/requires.js', ['Blockly.requires'], ['Blockly', 'Blockly.Comment', 'Blockly.ContextMenuItems', 'Blockly.FieldAngle', 'Blockly.FieldCheckbox', 'Blockly.FieldColour', 'Blockly.FieldDropdown', 'Blockly.FieldImage', 'Blockly.FieldLabelSerializable', 'Blockly.FieldMultilineInput', 'Blockly.FieldNumber', 'Blockly.FieldTextInput', 'Blockly.FieldVariable', 'Blockly.FlyoutButton', 'Blockly.Generator', 'Blockly.HorizontalFlyout', 'Blockly.Mutator', 'Blockly.ShortcutItems', 'Blockly.Themes.Classic', 'Blockly.Toolbox', 'Blockly.Trashcan', 'Blockly.VariablesDynamic', 'Blockly.VerticalFlyout', 'Blockly.Warning', 'Blockly.ZoomControls', 'Blockly.geras.Renderer', 'Blockly.serialization.blocks', 'Blockly.thrasos.Renderer', 'Blockly.zelos.Renderer']); goog.addDependency('../../core/scrollbar.js', ['Blockly.Scrollbar'], ['Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/scrollbar_pair.js', ['Blockly.ScrollbarPair'], ['Blockly.Events', 'Blockly.Scrollbar', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], [], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Xml', 'Blockly.inputTypes'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/serialization/serialization.js', ['Blockly.serialization'], [], {'module': 'goog'}); goog.addDependency('../../core/shortcut_items.js', ['Blockly.ShortcutItems'], ['Blockly.Gesture', 'Blockly.ShortcutRegistry', 'Blockly.clipboard', 'Blockly.common', 'Blockly.utils.KeyCodes'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/shortcut_registry.js', ['Blockly.ShortcutRegistry'], ['Blockly.utils.KeyCodes', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index 3b46355472b..c6fe395252b 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -343,5 +343,295 @@ suite('JSO', function() { assertProperty(jso, 'fields', {'FIELD': ['state1', 42, true]}); }); }); + + suite('Connected blocks', function() { + setup(function() { + this.assertInput = function(jso, name, value) { + chai.assert.deepInclude(jso['inputs'][name], value); + }; + + this.createBlockWithChild = function(blockType, inputName) { + const block = this.workspace.newBlock(blockType); + const childBlock = this.workspace.newBlock(blockType); + block.getInput(inputName).connection.connect( + childBlock.outputConnection || childBlock.previousConnection); + return block; + }; + + this.createBlockWithShadow = function(blockType, inputName) { + const block = this.workspace.newBlock(blockType); + block.getInput(inputName).connection.setShadowDom( + Blockly.Xml.textToDom( + '')); + return block; + }; + + this.createBlockWithShadowAndChild = function(blockType, inputName) { + const block = this.workspace.newBlock(blockType); + const childBlock = this.workspace.newBlock(blockType); + block.getInput(inputName).connection.connect( + childBlock.outputConnection || childBlock.previousConnection); + block.getInput(inputName).connection.setShadowDom( + Blockly.Xml.textToDom( + '')); + return block; + }; + + this.assertChild = function(blockType, inputName) { + const block = this.createBlockWithChild(blockType, inputName); + const jso = Blockly.serialization.blocks.save(block); + this.assertInput( + jso, inputName, {'block': { 'type': blockType, 'id': 'id2'}}); + }; + + this.assertShadow = function(blockType, inputName) { + const block = this.createBlockWithShadow(blockType, inputName); + const jso = Blockly.serialization.blocks.save(block); + this.assertInput( + jso, inputName, {'shadow': { 'type': blockType, 'id': 'test'}}); + }; + + this.assertOverwrittenShadow = function(blockType, inputName) { + const block = + this.createBlockWithShadowAndChild(blockType, inputName); + const jso = Blockly.serialization.blocks.save(block); + this.assertInput( + jso, + inputName, + { + 'block': { + 'type': blockType, + 'id': 'id2' + }, + 'shadow': { + 'type': blockType, + 'id': 'test' + } + }); + }; + + this.assertNoChild = function(blockType, inputName) { + const block = this.createBlockWithChild(blockType, inputName); + const jso = + Blockly.serialization.blocks.save(block, {addInputBlocks: false}); + chai.assert.isUndefined(jso['inputs']); + }; + + this.assertNoShadow = function(blockType, inputName) { + const block = this.createBlockWithShadow(blockType, inputName); + const jso = + Blockly.serialization.blocks.save(block, {addInputBlocks: false}); + chai.assert.isUndefined(jso['inputs']); + }; + + this.assertNoOverwrittenShadow = function(blockType, inputName) { + const block = + this.createBlockWithShadowAndChild(blockType, inputName); + const jso = + Blockly.serialization.blocks.save(block, {addInputBlocks: false}); + chai.assert.isUndefined(jso['inputs']); + }; + }); + + suite('Value', function() { + suite('With serialization', function() { + test('Child', function() { + this.assertChild('row_block', 'INPUT'); + }); + + test.skip('Shadow', function() { + this.assertShadow('row_block', 'INPUT'); + }); + + test.skip('Overwritten shadow', function() { + this.assertOverwrittenShadow('row_block', 'INPUT'); + }); + }); + + suite('Without serialization', function() { + test('Child', function() { + this.assertNoChild('row_block', 'INPUT'); + }); + + test('Shadow', function() { + this.assertNoShadow('row_block', 'INPUT'); + }); + + test('Overwritten shadow', function() { + this.assertNoOverwrittenShadow('row_block', 'INPUT'); + }); + }); + }); + + suite('Statement', function() { + suite('With serialization', function() { + test('Child', function() { + this.assertChild('statement_block', 'NAME'); + }); + + test.skip('Shadow', function() { + this.assertShadow('statement_block', 'NAME'); + }); + + test.skip('Overwritten shadow', function() { + this.assertOverwrittenShadow('statement_block', 'NAME'); + }); + + test('Child with next blocks', function() { + const block = this.workspace.newBlock('statement_block'); + const childBlock = this.workspace.newBlock('stack_block'); + const grandChildBlock = this.workspace.newBlock('stack_block'); + block.getInput('NAME').connection + .connect(childBlock.previousConnection); + childBlock.nextConnection + .connect(grandChildBlock.previousConnection); + const jso = Blockly.serialization.blocks.save(block); + this.assertInput( + jso, + 'NAME', + { + 'block': { + 'type': 'stack_block', + 'id': 'id2', + 'next': { + 'block': { + 'type': 'stack_block', + 'id': 'id4' + } + } + } + } + ); + }); + }); + + suite('Without serialization', function() { + test('Child', function() { + this.assertNoChild('statement_block', 'NAME'); + }); + + test('Shadow', function() { + this.assertNoShadow('statement_block', 'NAME'); + }); + + test('Overwritten shadow', function() { + this.assertNoOverwrittenShadow('statement_block', 'NAME'); + }); + }); + }); + + suite('Next', function() { + setup(function() { + this.createNextWithChild = function() { + const block = this.workspace.newBlock('stack_block'); + const childBlock = this.workspace.newBlock('stack_block'); + block.nextConnection.connect(childBlock.previousConnection); + return block; + }; + + this.createNextWithShadow = function() { + const block = this.workspace.newBlock('stack_block'); + block.nextConnection.setShadowDom( + Blockly.Xml.textToDom( + '')); + return block; + }; + + this.createNextWithShadowAndChild = function() { + const block = this.workspace.newBlock('stack_block'); + const childBlock = this.workspace.newBlock('stack_block'); + block.nextConnection.connect(childBlock.previousConnection); + block.nextConnection.setShadowDom( + Blockly.Xml.textToDom( + '')); + return block; + }; + }); + + suite('With serialization', function() { + test('Child', function() { + const block = this.createNextWithChild(); + const jso = + Blockly.serialization.blocks.save(block); + chai.assert.deepInclude( + jso['next'], {'block': { 'type': 'stack_block', 'id': 'id2'}}); + }); + + test.skip('Shadow', function() { + const block = this.createNextWithShadow(); + const jso = Blockly.serialization.blocks.save(block); + chai.assert.deepInclude( + jso['next'], {'shadow': { 'type': 'stack_block', 'id': 'test'}}); + }); + + test.skip('Overwritten shadow', function() { + const block = this.createNextWithShadowAndChild(); + const jso = Blockly.serialization.blocks.save(block); + chai.assert.deepInclude( + jso['next'], + { + 'block': { + 'type': 'stack_block', + 'id': 'id2' + }, + 'shadow': { + 'type': 'stack_block', + 'id': 'test' + } + }); + }); + + test('Next block with inputs', function() { + const block = this.workspace.newBlock('stack_block'); + const childBlock = this.workspace.newBlock('statement_block'); + const grandChildBlock = this.workspace.newBlock('stack_block'); + block.nextConnection.connect(childBlock.previousConnection); + childBlock.getInput('NAME').connection + .connect(grandChildBlock.previousConnection); + const jso = Blockly.serialization.blocks.save(block); + chai.assert.deepInclude( + jso['next'], + { + 'block': { + 'type': 'statement_block', + 'id': 'id2', + 'inputs': { + 'NAME': { + 'block': { + 'type': 'stack_block', + 'id': 'id4' + } + } + } + } + } + ); + }); + }); + + suite('Without serialization', function() { + test('Child', function() { + const block = this.createNextWithChild(); + const jso = Blockly.serialization.blocks.save( + block, {addNextBlocks: false}); + chai.assert.isUndefined(jso['next']); + }); + + test('Shadow', function() { + const block = this.createNextWithShadow(); + const jso = Blockly.serialization.blocks.save( + block, {addNextBlocks: false}); + chai.assert.isUndefined(jso['next']); + }); + + test('Overwritten shadow', function() { + const block = this.createNextWithShadowAndChild(); + const jso = Blockly.serialization.blocks.save( + block, {addNextBlocks: false}); + chai.assert.isUndefined(jso['next']); + }); + }); + }); + }); }); });