Skip to content
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.
* its 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');
227 changes: 201 additions & 26 deletions core/serialization/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,154 @@ 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 registry = goog.require('Blockly.serialization.registry');


// 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)
* }}
* Defines the state of a connection.
* @record
*/
var ConnectionState;
class ConnectionState {
constructor() {
/**
* The state of the shadow block attached to this connection, if any.
* @type {(!State|undefined)}
*/
this.shadow;

/**
* The state of the real block attached to this connection, if any.
* @type {(!State|undefined)}
*/
this.block;
}
}
exports.ConnectionState = ConnectionState;

/**
* Represents the state of a given block.
* @typedef {{
* type: string,
* id: string,
* x: (number|undefined),
* y: (number|undefined),
* collapsed: (boolean|undefined),
* disabled: (boolean|undefined),
* editable: (boolean|undefined),
* deletable: (boolean|undefined),
* movable: (boolean|undefined),
* inline: (boolean|undefined),
* data: (string|undefined),
* extra-state: *,
* icons: (!Object<string, *>|undefined),
* fields: (!Object<string, *>|undefined),
* inputs: (!Object<string, !ConnectionState>|undefined),
* next: (!ConnectionState|undefined)
* }}
* Defines the state of a block.
* @record
*/
var State;
class State {
constructor() {
/**
* The type of the block, eg "controls_if".
* @type {string}
*/
this.type;

/**
* The id of the block.
* @type {string}
*/
this.id;

/**
* The position of the block along the x axis.
* @type {(number|undefined)}
*/
this.x;

/**
* The position of the block along the y axis.
* @type {(number|undefined)}
*/
this.y;

/**
* Whether the block is collapsed or not. True if it is collapsed, undefined
* if not.
* @type {(boolean|undefined)}
*/
this.collapsed;

/**
* Whether the block is enabled or not. Undefined if it is enabled, false
* if it is not.
* @type {(boolean|undefined)}
*/
this.enabled;

/**
* Whether the block is editable or not. Undefined if it is editable, false
* if it is not.
* @type {(boolean|undefined)}
*/
this.editable;

/**
* Whether the block is deletable or not. Undefined if it is deletable,
* false if it is not.
* @type {(boolean|undefined)}
*/
this.deletable;

/**
* Whether the block is movable or not. Undefined if it is movable, false
* if it is not.
* @type {(boolean|undefined)}
*/
this.movable;

/**
* Whether the block is inline or not. True if it is inlined, false if it
* is not. Undefined if the inlined state matches the block's default
* inlined state (depends on the particular block).
* @type {(boolean|undefined)}
*/
this.inline;

/**
* An extra string that gets round-tripped through serialization. Often used
* for associating blocks with external assets.
* @type {(string|undefined)}
*/
this.data;

/**
* The extra state associated with the block.
* In the past this was called a "mutation".
* @type {(*|undefined)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't really think you need the |undefined since * means ALL which includes undefined.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird, what you said makes sense so I removed it, but it gave me a compiler error. Maybe |undefined isn't required in general, but it is when specifying optional properties?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess that makes sense too.

*/
this.extraState;

/**
* The state of the icons attached to this block.
* @type {(!Object<string, *>|undefined)}
*/
this.icons;

/**
* The state of the fields attached to this block.
* @type {(!Object<string, *>|undefined)}
*/
this.fields;

/**
* The state of the blocks and shadow blocks attached to the inputs of this
* block. Includes both value inputs and statement inputs.
* @type {(!Object<string, !ConnectionState>|undefined)}
*/
this.inputs;

/**
* The state of the block and/or shadow block attached to the next input
* of this block, if one exists.
* @type {(!ConnectionState|undefined)}
*/
this.next;
}
}
exports.State = State;

/**
Expand Down Expand Up @@ -577,3 +684,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);
}
}
}

registry.register('blocks', new BlockSerializer());
Loading