Skip to content
71 changes: 71 additions & 0 deletions core/interfaces/i_serializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @fileoverview The record type for an object containing functions for
* serializing part of the workspace.
*/

'use strict';

goog.module('Blockly.serialization.ISerializer');
goog.module.declareLegacyNamespace();

// eslint-disable-next-line no-unused-vars
const Workspace = goog.requireType('Blockly.Workspace');


/**
* Serializes and deserializes a plugin.
* @interface
*/
class ISerializer {
constructor() {
/**
* A priority value used to determine the order of deserializing plugins.
* More positive priorities are deserialized before less positive
* priorities. Eg if you have priorities (0, -10, 10, 100) the order of
* deserialiation will be (100, 10, 0, -10).
* If two plugins have the same priority, they are deserialized in an
* arbitrary order relative to each other.
* @type {number}
*/
this.priority;
}

/* eslint-disable no-unused-vars, valid-jsdoc */

/**
* Saves the state of the plugin.
* @param {!Workspace} workspace The workspace the plugin to serialize is
* associated with.
* @return {?} A JS object containing the plugin's state, or null if
* there is no state to record.
*/
save(workspace) {}

/* eslint-enable valid-jsdoc */

/**
* Loads the state of the plugin.
* @param {?} state The state of the plugin to deserialize. This will always
* be non-null.
* @param {!Workspace} workspace The workspace the plugin to deserialize is
* associated with.
*/
load(state, workspace) {}

/**
* Clears the state of the plugin.
* @param {!Workspace} workspace The workspace the plugin to clear the state
* of is associated with.
*/
clear(workspace) {}

/* eslint-enable no-unused-vars */
}

exports.ISerializer = ISerializer;
68 changes: 62 additions & 6 deletions core/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,26 @@ goog.requireType('Blockly.IToolbox');
goog.requireType('Blockly.Options');
goog.requireType('Blockly.Theme');
goog.requireType('Blockly.ToolboxItem');
goog.requireType('Blockly.serialization.ISerializer');


/**
* A map of maps. With the keys being the type and name of the class we are
* registering and the value being the constructor function.
* e.g. {'field': {'field_angle': Blockly.FieldAngle}}
*
* @type {Object<string, Object<string, function(new:?)>>}
* @type {!Object<string, !Object<string, (function(new:?)|!Object)>>}
*/
Blockly.registry.typeMap_ = Object.create(null);

/**
* A map of maps. With the keys being the type and caseless name of the class we
* are registring, and the value being the most recent cased name for that
* registration.
* @type {!Object<string, !Object<string, string>>}
*/
Blockly.registry.nameMap_ = Object.create(null);

/**
* The string used to register the default class for a type of plugin.
* @type {string}
Expand Down Expand Up @@ -106,6 +115,12 @@ Blockly.registry.Type.METRICS_MANAGER =
Blockly.registry.Type.BLOCK_DRAGGER =
new Blockly.registry.Type('blockDragger');

/**
* @type {!Blockly.registry.Type<Blockly.serialization.ISerializer>}
* @package
*/
Blockly.registry.Type.SERIALIZER = new Blockly.registry.Type('serializer');

/**
* Registers a class based on a type and name.
* @param {string|!Blockly.registry.Type<T>} type The type of the plugin.
Expand All @@ -117,7 +132,7 @@ Blockly.registry.Type.BLOCK_DRAGGER =
* an already registered item.
* @throws {Error} if the type or name is empty, a name with the given type has
* already been registered, or if the given class or object is not valid for
* it's type.
* it's type.
* @template T
*/
Blockly.registry.register = function(
Expand All @@ -135,25 +150,29 @@ Blockly.registry.register = function(
'Invalid name "' + name + '". The name must be a' +
' non-empty string.');
}
name = name.toLowerCase();
var caselessName = name.toLowerCase();
if (!registryItem) {
throw Error('Can not register a null value');
}
var typeRegistry = Blockly.registry.typeMap_[type];
var nameRegistry = Blockly.registry.nameMap_[type];
// If the type registry has not been created, create it.
if (!typeRegistry) {
typeRegistry = Blockly.registry.typeMap_[type] = Object.create(null);
nameRegistry = Blockly.registry.nameMap_[type] = Object.create(null);
}

// Validate that the given class has all the required properties.
Blockly.registry.validate_(type, registryItem);

// Don't throw an error if opt_allowOverrides is true.
if (!opt_allowOverrides && typeRegistry[name]) {
if (!opt_allowOverrides && typeRegistry[caselessName]) {
throw Error(
'Name "' + name + '" with type "' + type + '" already registered.');
'Name "' + caselessName + '" with type "' + type +
'" already registered.');
}
typeRegistry[name] = registryItem;
typeRegistry[caselessName] = registryItem;
nameRegistry[caselessName] = name;
};

/**
Expand Down Expand Up @@ -273,6 +292,43 @@ Blockly.registry.getObject = function(type, name, opt_throwIfMissing) {
Blockly.registry.getItem_(type, name, opt_throwIfMissing));
};

/**
* Returns a map of items registered with the given type.
* @param {string|!Blockly.registry.Type<T>} type The type of the plugin.
* (e.g. Category)
* @param {boolean} opt_cased Whether or not to return a map with cased keys
* (rather than caseless keys). False by default.
* @param {boolean=} opt_throwIfMissing Whether or not to throw an error if we
* are unable to find the object. False by default.
* @return {?Object<string, ?T|?function(new:T, ...?)>} A map of objects with
* the given type, or null if none exists.
* @template T
*/
Blockly.registry.getAllItems = function(type, opt_cased, opt_throwIfMissing) {
type = String(type).toLowerCase();
var typeRegistry = Blockly.registry.typeMap_[type];
if (!typeRegistry) {
var msg = `Unable to find [${type}] in the registry.`;
if (opt_throwIfMissing) {
throw new Error(`${msg} You must require or register a ${type} plugin.`);
} else {
console.warn(msg);
}
return null;
}
if (!opt_cased) {
return typeRegistry;
}
var nameRegistry = Blockly.registry.nameMap_[type];
var casedRegistry = Object.create(null);
var keys = Object.keys(typeRegistry);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
casedRegistry[nameRegistry[key]] = typeRegistry[key];
}
return casedRegistry;
};

/**
* Gets the class from Blockly options for the given type.
* This is used for plugins that override a built in feature. (e.g. Toolbox)
Expand Down
3 changes: 3 additions & 0 deletions core/requires.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,7 @@ goog.require('Blockly.zelos.Renderer');
// Classic is the default theme.
goog.require('Blockly.Themes.Classic');

goog.require('Blockly.serialization.blocks');
goog.require('Blockly.serialization.registry');
goog.require('Blockly.serialization.variables');
goog.require('Blockly.serialization.workspaces');
72 changes: 72 additions & 0 deletions core/serialization/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ const Block = goog.requireType('Blockly.Block');
// eslint-disable-next-line no-unused-vars
const Connection = goog.requireType('Blockly.Connection');
const Events = goog.require('Blockly.Events');
// eslint-disable-next-line no-unused-vars
const {ISerializer} = goog.requireType('Blockly.serialization.ISerializer');
const Size = goog.require('Blockly.utils.Size');
// eslint-disable-next-line no-unused-vars
const Workspace = goog.requireType('Blockly.Workspace');
const inputTypes = goog.require('Blockly.inputTypes');
const priorities = goog.require('Blockly.serialization.priorities');
const serializationRegistry = goog.require('Blockly.serialization.registry');


// TODO: Remove this once lint is fixed.
Expand Down Expand Up @@ -577,3 +581,71 @@ const initBlock = function(block, rendered) {
block.initModel();
}
};

// Aliases to disambiguate saving/loading within the serializer.
const saveBlock = save;
const loadBlock = load;

/**
* Serializer for saving and loading block state.
* @implements {ISerializer}
*/
class BlockSerializer {
constructor() {
/**
* The priority for deserializing blocks.
* @type {number}
*/
this.priority = priorities.BLOCKS;
}

/**
* Serializes the blocks of the given workspace.
* @param {!Workspace} workspace The workspace to save the blocks of.
* @return {?{languageVersion: number, blocks:!Array<!State>}} The state of
* the workspace's blocks, or null if there are no blocks.
*/
save(workspace) {
const blockState = [];
for (const block of workspace.getTopBlocks(false)) {
const state = saveBlock(block, {addCoordinates: true});
if (state) {
blockState.push(state);
}
}
if (blockState.length) {
return {
'languageVersion': 0, // Currently unused.
'blocks': blockState
};
}
return null;
}

/**
* Deserializes the blocks defined by the given state into the given
* workspace.
* @param {{languageVersion: number, blocks:!Array<!State>}} state The state
* of the blocks to deserialize.
* @param {!Workspace} workspace The workspace to deserialize into.
*/
load(state, workspace) {
const blockStates = state['blocks'];
for (const state of blockStates) {
loadBlock(state, workspace, {recordUndo: Events.recordUndo});
}
}

/**
* Disposes of any blocks that exist on the workspace.
* @param {!Workspace} workspace The workspace to clear the blocks of.
*/
clear(workspace) {
// Cannot use workspace.clear() because that also removes variables.
for (const block of workspace.getTopBlocks(false)) {
block.dispose(false);
}
}
}

serializationRegistry.register('blocks', new BlockSerializer());
34 changes: 34 additions & 0 deletions core/serialization/priorities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @fileoverview Includes constants for the priorities of different plugin
* serializers. Higher priorities are deserialized first.
*/

'use strict';

/**
* The top level namespace for priorities of plugin serializers.
* @namespace Blockly.serialization.priorities
*/
goog.module('Blockly.serialization.priorities');
goog.module.declareLegacyNamespace();


/**
* The priority for deserializing variables.
* @type {number}
* @const
*/
exports.VARIABLES = 100;

/**
* The priority for deserializing blocks.
* @type {number}
* @const
*/
exports.BLOCKS = 50;
40 changes: 40 additions & 0 deletions core/serialization/registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @fileoverview Contains functions registering serializers (eg blocks,
* variables, plugins, etc).
*/
'use strict';

goog.module('Blockly.serialization.registry');
goog.module.declareLegacyNamespace();


// eslint-disable-next-line no-unused-vars
const {ISerializer} = goog.requireType('Blockly.serialization.ISerializer');
const registry = goog.require('Blockly.registry');


/**
* Registers the given serializer so that it can be used for serialization and
* deserialization.
* @param {string} name The name of the serializer to register.
* @param {ISerializer} serializer The serializer to register.
*/
const register = function(name, serializer) {
registry.register(registry.Type.SERIALIZER, name, serializer);
};
exports.register = register;

/**
* Unregisters the serializer associated with the given name.
* @param {string} name The name of the serializer to unregister.
*/
const unregister = function(name) {
registry.unregister(registry.Type.SERIALIZER, name);
};
exports.unregister = unregister;
Loading