Skip to content

Commit

Permalink
feat: add deserialization of JSO block state (#5137)
Browse files Browse the repository at this point in the history
* Fixup tests

* Add deserialization of blocks

* Cleanup

* PR commnts
  • Loading branch information
BeksOmega committed Sep 17, 2021
1 parent 628ff7e commit 28378fd
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 9 deletions.
163 changes: 160 additions & 3 deletions core/serialization/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ const saveConnection = function(connection) {
state['shadow'] = Xml.domToText(shadow)
.replace('xmlns="https://developers.google.com/blockly/xml"', '');
}
if (child) {
if (child && !child.isShadow()) {
state['block'] = save(child);
}
return state;
Expand All @@ -263,8 +263,165 @@ const saveConnection = function(connection) {
* @param {!State} state The state of a block to deserialize into the workspace.
* @param {!Workspace} workspace The workspace to add the block to.
*/
// eslint-disable-next-line no-unused-vars
const load = function(state, workspace) {
// Temporarily NOP while connecting things together.
loadInternal(state, workspace);
};

/**
* Loads the block represented by the given state into the given workspace.
* This is defined internally so that the extra optional parameter doesn't
* clutter our external API.
* @param {!State} state The state of a block to deserialize into the workspace.
* @param {!Workspace} workspace The workspace to add the block to.
* @param {!Connection} parentConnection The optional parent connection to
* attach the block to.
*/
const loadInternal = function(state, workspace, parentConnection = undefined) {
const block = workspace.newBlock(state['type'], state['id']);
loadCoords(block, state);
loadAttributes(block, state);
loadExtraState(block, state);
if (parentConnection &&
(block.outputConnection || block.previousConnection)) {
parentConnection.connect(
/** @type {!Connection} */
(block.outputConnection || block.previousConnection));
}
// loadIcons(block, state);
loadFields(block, state);
loadInputBlocks(block, state);
loadNextBlocks(block, state);
};

/**
* Applies any coordinate information available on the state object to the
* block.
* @param {!Block} block The block to set the position of.
* @param {!State} state The state object to reference.
*/
const loadCoords = function(block, state) {
const x = state['x'] === undefined ? 10 : state['x'];
const y = state['y'] === undefined ? 10 : state['y'];
block.moveBy(x, y);
};

/**
* Applies any attribute information available on the state object to the block.
* @param {!Block} block The block to set the attributes of.
* @param {!State} state The state object to reference.
*/
const loadAttributes = function(block, state) {
if (state['collapsed']) {
block.setCollapsed(true);
}
if (state['enabled'] === false) {
block.setEnabled(false);
}
if (state['editable'] === false) {
block.setEditable(false);
}
if (state['deletable'] === false) {
block.setDeletable(false);
}
if (state['movable'] === false) {
block.setMovable(false);
}
if (state['inline'] !== undefined) {
block.setInputsInline(state['inline']);
}
if (state['data'] !== undefined) {
block.data = state['data'];
}
};

/**
* Applies any extra state information available on the state object to the
* block.
* @param {!Block} block The block to set the extra state of.
* @param {!State} state The state object to reference.
*/
const loadExtraState = function(block, state) {
if (!state['extraState']) {
return;
}
block.loadExtraState(state['extraState']);
};

/**
* Applies any field information available on the state object to the block.
* @param {!Block} block The block to set the field state of.
* @param {!State} state The state object to reference.
*/
const loadFields = function(block, state) {
if (!state['fields']) {
return;
}
const keys = Object.keys(state['fields']);
for (let i = 0; i < keys.length; i++) {
const fieldName = keys[i];
const fieldState = state['fields'][fieldName];
const field = block.getField(fieldName);
if (!field) {
console.warn(
`Ignoring non-existant field ${fieldName} in block ${block.type}`);
continue;
}
field.loadState(fieldState);
}
};

/**
* Creates any child blocks (attached to inputs) defined by the given state
* and attaches them to the given block.
* @param {!Block} block The block to attach input blocks to.
* @param {!State} state The state object to reference.
*/
const loadInputBlocks = function(block, state) {
if (!state['inputs']) {
return;
}
const keys = Object.keys(state['inputs']);
for (let i = 0; i < keys.length; i++) {
const inputName = keys[i];
const input = block.getInput(inputName);
if (input && input.connection) {
loadConnection(input.connection, state['inputs'][inputName]);
}
}
};

/**
* Creates any next blocks defined by the given state and attaches them to the
* given block.
* @param {!Block} block The block to attach next blocks to.
* @param {!State} state The state object to reference.
*/
const loadNextBlocks = function(block, state) {
if (!state['next']) {
return;
}
if (block.nextConnection) {
loadConnection(block.nextConnection, state['next']);
}
};

/**
* Applies the state defined by connectionState to the given connection, ie
* assigns shadows and attaches child blocks.
* @param {!Connection} connection The connection to serialize the
* connected blocks of.
* @param {!ConnectionState} connectionState The object containing the state of
* any connected shadow block, or any connected real block.
*/
const loadConnection = function(connection, connectionState) {
if (connectionState['shadow']) {
connection.setShadowDom(Blockly.Xml.textToDom(connectionState['shadow']));
}
if (connectionState['block']) {
loadInternal(
connectionState['block'],
connection.getSourceBlock().workspace,
connection);
}
};
exports.load = load;
13 changes: 7 additions & 6 deletions tests/mocha/serializer_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1264,7 +1264,7 @@ Serializer.Connections.OverwrittenShadow.Row = new SerializerTestCase('Row',
'</shadow>' +
'</value>' +
'</shadow>' +
'<block type="logic_boolean" id="id3*****************">' +
'<block type="logic_boolean" id="id4*****************">' +
'<field name="BOOL">TRUE</field>' +
'</block>' +
'</value>' +
Expand All @@ -1280,18 +1280,18 @@ Serializer.Connections.OverwrittenShadow.Nested = new SerializerTestCase(
'<shadow type="text_print" id="id3*****************"></shadow>' +
'</statement>' +
'</shadow>' +
'<block type="text_print" id="id3*****************"></block>' +
'<block type="text_print" id="id4*****************"></block>' +
'</statement>' +
'</block>' +
'</xml>');
Serializer.Connections.OverwrittenShadow.Stack = new SerializerTestCase('Stack',
'<xml xmlns="https://developers.google.com/blockly/xml">' +
'<block type="text_print" id="id******************" x="42" y="42">' +
'<next>' +
'<block type="text_print" id="id3*****************"></block>' +
'<shadow type="text_print" id="id2*****************">' +
'<block type="text_print" id="id2*****************"></block>' +
'<shadow type="text_print" id="id3*****************">' +
'<next>' +
'<shadow type="text_print" id="id3*****************"></shadow>' +
'<shadow type="text_print" id="id4*****************"></shadow>' +
'</next>' +
'</shadow>' +
'</next>' +
Expand Down Expand Up @@ -1772,5 +1772,6 @@ var runSerializerTestSuite = (serializer, deserializer, testSuite) => {
};

runSerializerTestSuite(null, null, Serializer);
Serializer.skip = true;
Serializer.Icons.skip = true;
Serializer.Mutations.skip = true;
runSerializerTestSuite(state => state, state => state, Serializer);

0 comments on commit 28378fd

Please sign in to comment.