diff --git a/core/interfaces/i_serializer.js b/core/interfaces/i_serializer.js new file mode 100644 index 00000000000..e7b75b9e400 --- /dev/null +++ b/core/interfaces/i_serializer.js @@ -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; diff --git a/core/registry.js b/core/registry.js index 2959cf73eba..a06cf34f4b7 100644 --- a/core/registry.js +++ b/core/registry.js @@ -25,6 +25,7 @@ goog.requireType('Blockly.IToolbox'); goog.requireType('Blockly.Options'); goog.requireType('Blockly.Theme'); goog.requireType('Blockly.ToolboxItem'); +goog.requireType('Blockly.serialization.ISerializer'); /** @@ -32,10 +33,18 @@ goog.requireType('Blockly.ToolboxItem'); * registering and the value being the constructor function. * e.g. {'field': {'field_angle': Blockly.FieldAngle}} * - * @type {Object>} + * @type {!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>} + */ +Blockly.registry.nameMap_ = Object.create(null); + /** * The string used to register the default class for a type of plugin. * @type {string} @@ -106,6 +115,12 @@ Blockly.registry.Type.METRICS_MANAGER = Blockly.registry.Type.BLOCK_DRAGGER = new Blockly.registry.Type('blockDragger'); +/** + * @type {!Blockly.registry.Type} + * @package + */ +Blockly.registry.Type.SERIALIZER = new Blockly.registry.Type('serializer'); + /** * Registers a class based on a type and name. * @param {string|!Blockly.registry.Type} type The type of the plugin. @@ -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( @@ -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; }; /** @@ -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} 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} 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) diff --git a/core/requires.js b/core/requires.js index 974db99fedc..7fdbffdb62c 100644 --- a/core/requires.js +++ b/core/requires.js @@ -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'); diff --git a/core/serialization/blocks.js b/core/serialization/blocks.js index 008f930b608..3fb1d9475c9 100644 --- a/core/serialization/blocks.js +++ b/core/serialization/blocks.js @@ -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. @@ -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}} 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 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()); diff --git a/core/serialization/priorities.js b/core/serialization/priorities.js new file mode 100644 index 00000000000..3535d518577 --- /dev/null +++ b/core/serialization/priorities.js @@ -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; diff --git a/core/serialization/registry.js b/core/serialization/registry.js new file mode 100644 index 00000000000..7806db29fda --- /dev/null +++ b/core/serialization/registry.js @@ -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; diff --git a/core/serialization/variables.js b/core/serialization/variables.js index 1984a63d04d..31364bf6477 100644 --- a/core/serialization/variables.js +++ b/core/serialization/variables.js @@ -15,9 +15,13 @@ goog.module.declareLegacyNamespace(); const Events = goog.require('Blockly.Events'); // eslint-disable-next-line no-unused-vars +const {ISerializer} = goog.requireType('Blockly.serialization.ISerializer'); +// eslint-disable-next-line no-unused-vars const VariableModel = goog.requireType('Blockly.VariableModel'); // eslint-disable-next-line no-unused-vars const Workspace = goog.requireType('Blockly.Workspace'); +const priorities = goog.require('Blockly.serialization.priorities'); +const serializationRegistry = goog.require('Blockly.serialization.registry'); /** @@ -36,7 +40,7 @@ exports.State = State; * @param {!VariableModel} variableModel The variable to serialize. * @return {!State} The serialized state of the variable. */ -const save = function(variableModel) { +const saveVariable = function(variableModel) { const state = { 'name': variableModel.name, 'id': variableModel.getId() @@ -46,12 +50,9 @@ const save = function(variableModel) { } return state; }; -/** @package */ -exports.save = save; /** * Loads the variable represented by the given state into the given workspace. - * Do not call this directly, use workspace.createVariable instead. * @param {!State} state The state of a variable to deserialize into the * workspace. * @param {!Workspace} workspace The workspace to add the variable to. @@ -59,7 +60,7 @@ exports.save = save; * recordUndo: If true, events triggered by this function will be undo-able * by the user. False by default. */ -const load = function(state, workspace, {recordUndo = false} = {}) { +const loadVariable = function(state, workspace, {recordUndo = false} = {}) { const prevRecordUndo = Events.recordUndo; Events.recordUndo = recordUndo; const existingGroup = Events.getGroup(); @@ -72,5 +73,54 @@ const load = function(state, workspace, {recordUndo = false} = {}) { Events.setGroup(existingGroup); Events.recordUndo = prevRecordUndo; }; -/** @package */ -exports.load = load; + +/** + * Serializer for saving and loading variable state. + * @implements {ISerializer} + */ +class VariableSerializer { + constructor() { + /** + * The priority for deserializing variables. + * @type {number} + */ + this.priority = priorities.VARIABLES; + } + + /** + * Serializes the variables of the given workspace. + * @param {!Workspace} workspace The workspace to save the variables of. + * @return {?Array} The state of the workspace's variables, or null + * if there are no variables. + */ + save(workspace) { + const variableStates = []; + for (const variable of workspace.getAllVariables()) { + variableStates.push(saveVariable(variable)); + } + return variableStates.length ? variableStates : null; + } + + /** + * Deserializes the variable defined by the given state into the given + * workspace. + * @param {!Array} state The state of the variables to deserialize. + * @param {!Workspace} workspace The workspace to deserialize into. + */ + load(state, workspace) { + for (const varState of state) { + loadVariable(varState, workspace, {recordUndo: Events.recordUndo}); + } + } + + /** + * Disposes of any variables or potential variables that exist on the + * workspace. + * @param {!Workspace} workspace The workspace to clear the variables of. + */ + clear(workspace) { + workspace.getVariableMap().clear(); + } +} + +serializationRegistry.register('variables', new VariableSerializer()); diff --git a/core/serialization/workspaces.js b/core/serialization/workspaces.js index 38e6d2069bb..f3100458e4d 100644 --- a/core/serialization/workspaces.js +++ b/core/serialization/workspaces.js @@ -16,9 +16,8 @@ goog.module.declareLegacyNamespace(); const Events = goog.require('Blockly.Events'); // eslint-disable-next-line no-unused-vars const Workspace = goog.require('Blockly.Workspace'); -const blocks = goog.require('Blockly.serialization.blocks'); const dom = goog.require('Blockly.utils.dom'); -const variables = goog.require('Blockly.serialization.variables'); +const registry = goog.require('Blockly.registry'); /** @@ -28,31 +27,13 @@ const variables = goog.require('Blockly.serialization.variables'); */ const save = function(workspace) { const state = Object.create(null); - - // TODO: Switch this to use plugin serialization system (once it is built). - const variableStates = []; - const vars = workspace.getAllVariables(); - for (let i = 0; i < vars.length; i++) { - variableStates.push(variables.save(vars[i])); - } - if (variableStates.length) { - state['variables'] = variableStates; - } - - const blockStates = []; - for (let block of workspace.getTopBlocks(false)) { - const blockState = blocks.save(block, {addCoordinates: true}); - if (blockState) { - blockStates.push(blockState); + const serializerMap = registry.getAllItems(registry.Type.SERIALIZER, true); + for (const key in serializerMap) { + const save = serializerMap[key].save(workspace); + if (save) { + state[key] = save; } } - if (blockStates.length) { - // This is an object to support adding language version later. - state['blocks'] = { - 'blocks': blockStates - }; - } - return state; }; exports.save = save; @@ -67,8 +48,14 @@ exports.save = save; * by the user. False by default. */ const load = function(state, workspace, {recordUndo = false} = {}) { - // TODO: Switch this to use plugin serialization system (once it is built). - // TODO: Add something for clearing the state before deserializing. + const serializerMap = registry.getAllItems(registry.Type.SERIALIZER, true); + if (!serializerMap) { + return; + } + + const deserializers = Object.entries(serializerMap) + .sort(([, {priority: priorityA}], [, {priority: priorityB}]) => + priorityB - priorityA); const prevRecordUndo = Events.recordUndo; Events.recordUndo = recordUndo; @@ -81,18 +68,19 @@ const load = function(state, workspace, {recordUndo = false} = {}) { if (workspace.setResizesEnabled) { workspace.setResizesEnabled(false); } - - if (state['variables']) { - const variableStates = state['variables']; - for (let i = 0; i < variableStates.length; i++) { - variables.load(variableStates[i], workspace, {recordUndo}); - } + + // We want to trigger clearing in reverse priority order so plugins don't end + // up missing dependencies. + for (const [, deserializer] of deserializers.reverse()) { + deserializer.clear(workspace); } - if (state['blocks']) { - const blockStates = state['blocks']['blocks']; - for (let i = 0; i < blockStates.length; i++) { - blocks.load(blockStates[i], workspace, {recordUndo}); + // reverse() is destructive, so we have to re-reverse to correct the order. + for (let [name, deserializer] of deserializers.reverse()) { + name = /** @type {string} */ (name); + const pluginState = state[name]; + if (pluginState) { + deserializer.load(state[name], workspace); } } diff --git a/tests/deps.js b/tests/deps.js index 728a553fabd..798a29444e7 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -19,7 +19,7 @@ goog.addDependency('../../core/bubble.js', ['Blockly.Bubble'], ['Blockly.IBubble goog.addDependency('../../core/bubble_dragger.js', ['Blockly.BubbleDragger'], ['Blockly.Bubble', 'Blockly.ComponentManager', 'Blockly.Events', 'Blockly.Events.CommentMove', 'Blockly.constants', 'Blockly.utils', 'Blockly.utils.Coordinate']); goog.addDependency('../../core/comment.js', ['Blockly.Comment'], ['Blockly.Bubble', 'Blockly.Css', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Icon', 'Blockly.Warning', 'Blockly.browserEvents', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent']); goog.addDependency('../../core/component_manager.js', ['Blockly.ComponentManager'], []); -goog.addDependency('../../core/connection.js', ['Blockly.Connection'], ['Blockly.Events', 'Blockly.Events.BlockMove', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.Xml', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.utils.deprecation']); +goog.addDependency('../../core/connection.js', ['Blockly.Connection'], ['Blockly.Events', 'Blockly.Events.BlockMove', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.Xml', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.serialization.blocks', 'Blockly.utils.deprecation'], {'lang': 'es6'}); goog.addDependency('../../core/connection_checker.js', ['Blockly.ConnectionChecker'], ['Blockly.Connection', 'Blockly.IConnectionChecker', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.registry']); goog.addDependency('../../core/connection_db.js', ['Blockly.ConnectionDB'], ['Blockly.RenderedConnection', 'Blockly.connectionTypes', 'Blockly.constants']); goog.addDependency('../../core/connection_types.js', ['Blockly.connectionTypes'], []); @@ -111,7 +111,7 @@ goog.addDependency('../../core/names.js', ['Blockly.Names'], ['Blockly.Msg', 'Bl goog.addDependency('../../core/options.js', ['Blockly.Options'], ['Blockly.Theme', 'Blockly.Themes.Classic', 'Blockly.registry', 'Blockly.utils.IdGenerator', 'Blockly.utils.Metrics', 'Blockly.utils.toolbox']); goog.addDependency('../../core/positionable_helpers.js', ['Blockly.uiPosition'], ['Blockly.Scrollbar', 'Blockly.utils.Rect', 'Blockly.utils.toolbox']); goog.addDependency('../../core/procedures.js', ['Blockly.Procedures'], ['Blockly.Blocks', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.Names', 'Blockly.Workspace', 'Blockly.Xml', 'Blockly.constants', 'Blockly.utils.xml']); -goog.addDependency('../../core/registry.js', ['Blockly.registry'], []); +goog.addDependency('../../core/registry.js', ['Blockly.registry'], [], {'lang': 'es6'}); goog.addDependency('../../core/rendered_connection.js', ['Blockly.RenderedConnection'], ['Blockly.Connection', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.deprecation', 'Blockly.utils.dom', 'Blockly.utils.object']); goog.addDependency('../../core/renderers/common/block_rendering.js', ['Blockly.blockRendering'], ['Blockly.registry']); goog.addDependency('../../core/renderers/common/constants.js', ['Blockly.blockRendering.ConstantProvider'], ['Blockly.connectionTypes', 'Blockly.constants', 'Blockly.utils', 'Blockly.utils.Svg', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.svgPaths', 'Blockly.utils.userAgent'], {'lang': 'es5'}); @@ -151,12 +151,15 @@ goog.addDependency('../../core/renderers/zelos/measurables/row_elements.js', ['B goog.addDependency('../../core/renderers/zelos/measurables/rows.js', ['Blockly.zelos.BottomRow', 'Blockly.zelos.TopRow'], ['Blockly.blockRendering.BottomRow', 'Blockly.blockRendering.TopRow', 'Blockly.utils.object']); goog.addDependency('../../core/renderers/zelos/path_object.js', ['Blockly.zelos.PathObject'], ['Blockly.blockRendering.PathObject', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.zelos.ConstantProvider']); goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Renderer'], ['Blockly.InsertionMarkerManager', 'Blockly.blockRendering', 'Blockly.blockRendering.Renderer', 'Blockly.connectionTypes', 'Blockly.constants', 'Blockly.utils.object', 'Blockly.zelos.ConstantProvider', 'Blockly.zelos.Drawer', 'Blockly.zelos.MarkerSvg', 'Blockly.zelos.PathObject', 'Blockly.zelos.RenderInfo']); -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.workspaces', 'Blockly.thrasos.Renderer', 'Blockly.zelos.Renderer']); +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.serialization.registry', 'Blockly.serialization.variables', 'Blockly.serialization.workspaces', 'Blockly.thrasos.Renderer', 'Blockly.zelos.Renderer']); goog.addDependency('../../core/scrollbar.js', ['Blockly.Scrollbar', 'Blockly.ScrollbarPair'], ['Blockly.Events', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Svg', 'Blockly.utils.dom']); -goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Events', 'Blockly.Xml', 'Blockly.inputTypes', 'Blockly.serialization.exceptions'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Events', 'Blockly.inputTypes', 'Blockly.serialization.exceptions', 'Blockly.serialization.priorities', 'Blockly.serialization.registry', 'Blockly.utils.Size'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/serialization/exceptions.js', ['Blockly.serialization.exceptions'], [], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], ['Blockly.Events'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/workspaces.js', ['Blockly.serialization.workspaces'], ['Blockly.Events', 'Blockly.Workspace', 'Blockly.serialization.blocks', 'Blockly.serialization.variables', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/i_serializer.js', ['Blockly.serialization.ISerializer'], [], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/priorities.js', ['Blockly.serialization.priorities'], [], {'module': 'goog'}); +goog.addDependency('../../core/serialization/registry.js', ['Blockly.serialization.registry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], ['Blockly.Events', 'Blockly.serialization.priorities', 'Blockly.serialization.registry'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/workspaces.js', ['Blockly.serialization.workspaces'], ['Blockly.Events', 'Blockly.Workspace', 'Blockly.registry', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/shortcut_items.js', ['Blockly.ShortcutItems'], ['Blockly.Gesture', 'Blockly.ShortcutRegistry', 'Blockly.utils.KeyCodes']); goog.addDependency('../../core/shortcut_registry.js', ['Blockly.ShortcutRegistry'], ['Blockly.utils.KeyCodes', 'Blockly.utils.object']); goog.addDependency('../../core/theme.js', ['Blockly.Theme'], ['Blockly.registry', 'Blockly.utils', 'Blockly.utils.object']); diff --git a/tests/mocha/jso_deserialization_test.js b/tests/mocha/jso_deserialization_test.js index 7c80428f28a..adade141b26 100644 --- a/tests/mocha/jso_deserialization_test.js +++ b/tests/mocha/jso_deserialization_test.js @@ -89,188 +89,128 @@ suite('JSO Deserialization', function() { }); suite('Var create', function() { - suite('Top-level call', function() { - test('Just var', function() { - const state = { - 'variables': [ - { - 'name': 'test', - 'id': 'testId', - } - ] - }; - Blockly.serialization.workspaces.load(state, this.workspace); - assertEventFired( - this.eventsFireStub, - Blockly.Events.VarCreate, - { - 'varName': 'test', - 'varId': 'testId', - 'varType': '', - 'recordUndo': false - }, - this.workspace.id); - }); - - test('Record undo', function() { - const state = { - 'variables': [ - { - 'name': 'test', - 'id': 'testId', - } - ] - }; - Blockly.serialization.workspaces.load(state, this.workspace, {recordUndo: true}); - assertEventFired( - this.eventsFireStub, - Blockly.Events.VarCreate, - { - 'varName': 'test', - 'varId': 'testId', - 'varType': '', - 'recordUndo': true - }, - this.workspace.id); - }); - - test('Grouping', function() { - const state = { - 'variables': [ - { - 'name': 'test', - 'id': 'testId', - } - ] - }; - Blockly.Events.setGroup('my group'); - Blockly.serialization.workspaces.load(state, this.workspace); - assertEventFired( - this.eventsFireStub, - Blockly.Events.VarCreate, - { - 'varName': 'test', - 'varId': 'testId', - 'varType': '', - 'group': 'my group' - }, - this.workspace.id); - }); - - test('Multiple vars grouped', function() { - const state = { - 'variables': [ - { - 'name': 'test', - 'id': 'testId', - }, - { - 'name': 'test2', - 'id': 'testId2', - } - ] - }; - Blockly.serialization.workspaces.load(state, this.workspace); - const calls = this.eventsFireStub.getCalls(); - const group = calls[0].args[0].group; - chai.assert.isTrue(calls.every(call => call.args[0].group == group)); - }); - - test('Var with block', function() { - const state = { - 'variables': [ - { - 'name': 'test', - 'id': 'testId', - } - ], - 'blocks': { - 'blocks': [ - { - 'type': 'variables_get', - 'id': 'blockId', - 'x': 42, - 'y': 42, - 'fields': { - 'VAR': 'testId' - } - }, - ] + test('Just var', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', } - }; - Blockly.serialization.workspaces.load(state, this.workspace); - const calls = this.eventsFireStub.getCalls(); - const count = calls.reduce((acc, call) => { - if (call.args[0] instanceof Blockly.Events.VarCreate) { - return acc + 1; + ] + }; + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'recordUndo': false + }, + this.workspace.id); + }); + + test('Record undo', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', } - return acc; - }, 0); - chai.assert.equal(count, 1); - assertEventFired( - this.eventsFireStub, - Blockly.Events.VarCreate, - {'varName': 'test', 'varId': 'testId', 'varType': ''}, - this.workspace.id); - }); + ] + }; + Blockly.serialization.workspaces.load(state, this.workspace, {recordUndo: true}); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'recordUndo': true + }, + this.workspace.id); }); - suite('Direct call', function() { - test('Just var', function() { - const state = { - 'name': 'test', - 'id': 'testId', - }; - Blockly.serialization.variables.load(state, this.workspace); - assertEventFired( - this.eventsFireStub, - Blockly.Events.VarCreate, - { - 'varName': 'test', - 'varId': 'testId', - 'varType': '', - 'recordUndo': false - }, - this.workspace.id); - }); + test('Grouping', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', + } + ] + }; + Blockly.Events.setGroup('my group'); + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'group': 'my group' + }, + this.workspace.id); + }); - test('Record undo', function() { - const state = { - 'name': 'test', - 'id': 'testId', - }; - Blockly.serialization.variables - .load(state, this.workspace, {recordUndo: true}); - assertEventFired( - this.eventsFireStub, - Blockly.Events.VarCreate, - { - 'varName': 'test', - 'varId': 'testId', - 'varType': '', - 'recordUndo': true - }, - this.workspace.id); - }); + test('Multiple vars grouped', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', + }, + { + 'name': 'test2', + 'id': 'testId2', + } + ] + }; + Blockly.serialization.workspaces.load(state, this.workspace); + const calls = this.eventsFireStub.getCalls(); + const group = calls[0].args[0].group; + chai.assert.isTrue(calls.every(call => call.args[0].group == group)); + }); - test('Grouping', function() { - const state = { - 'name': 'test', - 'id': 'testId', - }; - Blockly.Events.setGroup('my group'); - Blockly.serialization.variables.load(state, this.workspace); - assertEventFired( - this.eventsFireStub, - Blockly.Events.VarCreate, + test('Var with block', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', + } + ], + 'blocks': { + 'blocks': [ { - 'varName': 'test', - 'varId': 'testId', - 'varType': '', - 'group': 'my group' + 'type': 'variables_get', + 'id': 'blockId', + 'x': 42, + 'y': 42, + 'fields': { + 'VAR': 'testId' + } }, - this.workspace.id); - }); + ] + } + }; + Blockly.serialization.workspaces.load(state, this.workspace); + const calls = this.eventsFireStub.getCalls(); + const count = calls.reduce((acc, call) => { + if (call.args[0] instanceof Blockly.Events.VarCreate) { + return acc + 1; + } + return acc; + }, 0); + chai.assert.equal(count, 1); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + {'varName': 'test', 'varId': 'testId', 'varType': ''}, + this.workspace.id); }); }); @@ -676,4 +616,60 @@ suite('JSO Deserialization', function() { }); }); }); + + test('Priority', function() { + const blocksSerializer = Blockly.registry.getClass( + Blockly.registry.Type.SERIALIZER, 'blocks'); + const variablesSerializer = Blockly.registry.getClass( + Blockly.registry.Type.SERIALIZER, 'variables'); + + Blockly.serialization.registry.unregister('blocks'); + Blockly.serialization.registry.unregister('variables'); + + const calls = []; + + const first = { + priority: 100, + save: () => null, + load: () => calls.push('first-load'), + clear: () => calls.push('first-clear'), + }; + const second = { + priority: 0, + save: () => null, + load: () => calls.push('second-load'), + clear: () => calls.push('second-clear'), + }; + const third = { + priority: -10, + save: () => null, + load: () => calls.push('third-load'), + clear: () => calls.push('third-clear'), + }; + + Blockly.serialization.registry.register('third', third); + Blockly.serialization.registry.register('first', first); + Blockly.serialization.registry.register('second', second); + + Blockly.serialization.workspaces.load( + {'first': {}, 'third': {}, 'second': {}}, this.workspace); + + Blockly.serialization.registry.unregister('first'); + Blockly.serialization.registry.unregister('second'); + Blockly.serialization.registry.unregister('third'); + + Blockly.serialization.registry.register('blocks', blocksSerializer); + Blockly.serialization.registry.register('variables', variablesSerializer); + + chai.assert.deepEqual( + calls, + [ + 'third-clear', + 'second-clear', + 'first-clear', + 'first-load', + 'second-load', + 'third-load' + ]); + }); }); diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index e0c978cec35..1848720d401 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -694,20 +694,21 @@ suite('JSO Serialization', function() { suite('Variables', function() { test('Without type', function() { - const variable = this.workspace.createVariable('testVar', '', 'testId'); - const jso = Blockly.serialization.variables.save(variable); - assertProperty(jso, 'name', 'testVar'); - assertProperty(jso, 'id', 'testId'); - assertNoProperty(jso, 'type'); + this.workspace.createVariable('testVar', '', 'testId'); + const jso = Blockly.serialization.workspaces.save(this.workspace); + const variable = jso['variables'][0]; + assertProperty(variable, 'name', 'testVar'); + assertProperty(variable, 'id', 'testId'); + assertNoProperty(variable, 'type'); }); test('With type', function() { - const variable = this.workspace - .createVariable('testVar', 'testType', 'testId'); - const jso = Blockly.serialization.variables.save(variable); - assertProperty(jso, 'name', 'testVar'); - assertProperty(jso, 'id', 'testId'); - assertProperty(jso, 'type', 'testType'); + this.workspace.createVariable('testVar', 'testType', 'testId'); + const jso = Blockly.serialization.workspaces.save(this.workspace); + const variable = jso['variables'][0]; + assertProperty(variable, 'name', 'testVar'); + assertProperty(variable, 'id', 'testId'); + assertProperty(variable, 'type', 'testType'); }); }); }); diff --git a/tests/mocha/registry_test.js b/tests/mocha/registry_test.js index 1ffff755ac6..f891bd0cdc7 100644 --- a/tests/mocha/registry_test.js +++ b/tests/mocha/registry_test.js @@ -10,6 +10,7 @@ */ 'use strict'; + suite('Registry', function() { var TestClass = function() {}; TestClass.prototype.testMethod = function() { @@ -19,39 +20,202 @@ suite('Registry', function() { setup(function() { sharedTestSetup.call(this); }); + teardown(function() { sharedTestTeardown.call(this); if (Blockly.registry.typeMap_['test'] && Blockly.registry.typeMap_['test']['test_name']) { delete Blockly.registry.typeMap_['test']['test_name']; + delete Blockly.registry.typeMap_['test']; + delete Blockly.registry.nameMap_['test']['test_name']; + delete Blockly.registry.nameMap_['test']; } }); + suite('Registration', function() { test('Simple', function() { Blockly.registry.register('test', 'test_name', TestClass); }); + test('Empty String Key', function() { chai.assert.throws(function() { Blockly.registry.register('test', '', TestClass); }, 'Invalid name'); }); + test('Class as Key', function() { chai.assert.throws(function() { Blockly.registry.register('test', TestClass, ''); }, 'Invalid name'); }); + test('Overwrite a Key', function() { Blockly.registry.register('test', 'test_name', TestClass); chai.assert.throws(function() { Blockly.registry.register('test', 'test_name', TestClass); }, 'already registered'); }); + test('Null Value', function() { chai.assert.throws(function() { Blockly.registry.register('test', 'field_custom_test', null); }, 'Can not register a null value'); }); }); + + suite('hasItem', function() { + setup(function() { + Blockly.registry.register('test', 'test_name', TestClass); + }); + + test('Has', function() { + chai.assert.isTrue(Blockly.registry.hasItem('test', 'test_name')); + }); + + suite('Does not have', function() { + test('Type', function() { + chai.assert.isFalse(Blockly.registry.hasItem('bad_type', 'test_name')); + }); + + test('Name', function() { + chai.assert.isFalse(Blockly.registry.hasItem('test', 'bad_name')); + }); + }); + + suite('Case', function() { + test('Caseless type', function() { + chai.assert.isTrue(Blockly.registry.hasItem('TEST', 'test_name')); + }); + + test('Caseless name', function() { + chai.assert.isTrue(Blockly.registry.hasItem('test', 'TEST_NAME')); + }); + }); + }); + + suite('getClass', function() { + setup(function() { + Blockly.registry.register('test', 'test_name', TestClass); + }); + + test('Has', function() { + chai.assert.isNotNull(Blockly.registry.getClass('test', 'test_name')); + }); + + suite('Does not have', function() { + test('Type', function() { + chai.assert.isNull(Blockly.registry.getClass('bad_type', 'test_name')); + }); + + test('Name', function() { + chai.assert.isNull(Blockly.registry.getClass('test', 'bad_name')); + }); + + test('Throw if missing', function() { + chai.assert.throws(function() { + Blockly.registry.getClass('test', 'bad_name', true); + }); + }); + }); + + suite('Case', function() { + test('Caseless type', function() { + chai.assert.isNotNull(Blockly.registry.getClass('TEST', 'test_name')); + }); + + test('Caseless name', function() { + chai.assert.isNotNull(Blockly.registry.getClass('test', 'TEST_NAME')); + }); + }); + }); + + suite('getObject', function() { + setup(function() { + Blockly.registry.register('test', 'test_name', {}); + }); + + test('Has', function() { + chai.assert.isNotNull(Blockly.registry.getObject('test', 'test_name')); + }); + + suite('Does not have', function() { + test('Type', function() { + chai.assert.isNull(Blockly.registry.getObject('bad_type', 'test_name')); + }); + + test('Name', function() { + chai.assert.isNull(Blockly.registry.getObject('test', 'bad_name')); + }); + + test('Throw if missing', function() { + chai.assert.throws(function() { + Blockly.registry.getObject('test', 'bad_name', true); + }); + }); + }); + + suite('Case', function() { + test('Caseless type', function() { + chai.assert.isNotNull(Blockly.registry.getObject('TEST', 'test_name')); + }); + + test('Caseless name', function() { + chai.assert.isNotNull(Blockly.registry.getObject('test', 'TEST_NAME')); + }); + }); + }); + + suite('getAllItems', function() { + setup(function() { + Blockly.registry.register('test', 'test_name', {}); + Blockly.registry.register('test', 'casedNAME', {}); + }); + + teardown(function() { + delete Blockly.registry.typeMap_['test']['casedname']; + delete Blockly.registry.typeMap_['test']; + delete Blockly.registry.nameMap_['test']['casedname']; + delete Blockly.registry.nameMap_['test']; + }); + + test('Has', function() { + chai.assert.isNotNull(Blockly.registry.getAllItems('test')); + }); + + test('Does not have', function() { + chai.assert.isNull(Blockly.registry.getAllItems('bad_type')); + }); + + test('Throw if missing', function() { + chai.assert.throws(function() { + Blockly.registry.getAllItems('bad_type', false, true); + }); + }); + + test('Ignore type case', function() { + chai.assert.isNotNull(Blockly.registry.getAllItems('TEST')); + }); + + test('Respect name case', function() { + chai.assert.deepEqual( + Blockly.registry.getAllItems('test', true), + { + 'test_name': {}, + 'casedNAME': {} + }); + }); + + test('Respect overwriting name case', function() { + Blockly.registry.register('test', 'CASEDname', {}, true); + chai.assert.deepEqual( + Blockly.registry.getAllItems('test', true), + { + 'test_name': {}, + 'CASEDname': {} + }); + }); + }); + suite('getClassFromOptions', function() { setup(function() { this.defaultClass = function() {}; @@ -63,25 +227,27 @@ suite('Registry', function() { 'test' : 'test_name' } }; - Blockly.registry.typeMap_['test'] = { - 'test_name': TestClass, - 'default': this.defaultClass - }; + Blockly.registry.register('test', 'test_name', TestClass); + Blockly.registry.register('test', 'default', this.defaultClass); }); + test('Simple - Plugin name given', function() { var testClass = Blockly.registry.getClassFromOptions('test', this.options); chai.assert.instanceOf(new testClass(), TestClass); }); + test('Simple - Plugin class given', function() { this.options.plugins['test'] = TestClass; var testClass = Blockly.registry.getClassFromOptions('test', this.options); chai.assert.instanceOf(new testClass(), TestClass); }); + test('No Plugin Name Given', function() { delete this.options['plugins']['test']; var testClass = Blockly.registry.getClassFromOptions('test', this.options); chai.assert.instanceOf(new testClass(), this.defaultClass); }); + test('Incorrect Plugin Name', function() { this.options['plugins']['test'] = 'random'; var testClass;