From b01b0421dd452e443377e02f980d4905f39d577f Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 10 Feb 2022 11:30:04 -0800 Subject: [PATCH 1/9] refactor: move optional function declarations into the constructor --- core/options.js | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/core/options.js b/core/options.js index af7ea45b64f..b3818fb0a45 100644 --- a/core/options.js +++ b/core/options.js @@ -187,6 +187,21 @@ const Options = function(options) { * @type {!Object} */ this.plugins = plugins; + + /** + * If set, sets the translation of the workspace to match the scrollbars. + * @type {undefined|function(!{x:number,y:number}):void} A function that sets + * the translation of the workspace to match the scrollbars. The argument + * Contains an x and/or y property which is a float between 0 and 1 + * specifying the degree of scrolling. + */ + this.setMetrics = undefined; + + /** + * @type {undefined|function():!Metrics} A function that returns a metrics + * object that describes the current workspace. + */ + this.getMetrics = undefined; }; /** @@ -233,20 +248,6 @@ Options.ScrollbarOptions; */ Options.ZoomOptions; -/** - * If set, sets the translation of the workspace to match the scrollbars. - * @param {!{x:number,y:number}} xyRatio Contains an x and/or y property which - * is a float between 0 and 1 specifying the degree of scrolling. - * @return {void} - */ -Options.prototype.setMetrics; - -/** - * Return an object with the metrics required to size the workspace. - * @return {!Metrics} Contains size and position metrics. - */ -Options.prototype.getMetrics; - /** * Parse the user-specified move options, using reasonable defaults where * behaviour is unspecified. From 300c2598fe311a2d1e3eafd256283e5098dba2c1 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 10 Feb 2022 11:41:57 -0800 Subject: [PATCH 2/9] refactor: convert options.js to es6 class --- core/options.js | 567 ++++++++++++++++++++++++------------------------ 1 file changed, 285 insertions(+), 282 deletions(-) diff --git a/core/options.js b/core/options.js index b3818fb0a45..fb28ec919f3 100644 --- a/core/options.js +++ b/core/options.js @@ -31,177 +31,315 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg'); /** * Parse the user-specified options, using reasonable defaults where behaviour * is unspecified. - * @param {!BlocklyOptions} options Dictionary of options. - * Specification: - * https://developers.google.com/blockly/guides/get-started/web#configuration - * @constructor - * @alias Blockly.Options */ -const Options = function(options) { - let toolboxJsonDef = null; - let hasCategories = false; - let hasTrashcan = false; - let hasCollapse = false; - let hasComments = false; - let hasDisable = false; - let hasSounds = false; - const readOnly = !!options['readOnly']; - if (!readOnly) { - toolboxJsonDef = toolbox.convertToolboxDefToJson(options['toolbox']); - hasCategories = toolbox.hasCategories(toolboxJsonDef); - hasTrashcan = options['trashcan']; - if (hasTrashcan === undefined) { - hasTrashcan = hasCategories; - } - hasCollapse = options['collapse']; - if (hasCollapse === undefined) { - hasCollapse = hasCategories; +class Options { + /** + * @param {!BlocklyOptions} options Dictionary of options. + * Specification: + * https://developers.google.com/blockly/guides/get-started/web#configuration + * @alias Blockly.Options + */ + constructor(options) { + let toolboxJsonDef = null; + let hasCategories = false; + let hasTrashcan = false; + let hasCollapse = false; + let hasComments = false; + let hasDisable = false; + let hasSounds = false; + const readOnly = !!options['readOnly']; + if (!readOnly) { + toolboxJsonDef = toolbox.convertToolboxDefToJson(options['toolbox']); + hasCategories = toolbox.hasCategories(toolboxJsonDef); + hasTrashcan = options['trashcan']; + if (hasTrashcan === undefined) { + hasTrashcan = hasCategories; + } + hasCollapse = options['collapse']; + if (hasCollapse === undefined) { + hasCollapse = hasCategories; + } + hasComments = options['comments']; + if (hasComments === undefined) { + hasComments = hasCategories; + } + hasDisable = options['disable']; + if (hasDisable === undefined) { + hasDisable = hasCategories; + } + hasSounds = options['sounds']; + if (hasSounds === undefined) { + hasSounds = true; + } } - hasComments = options['comments']; - if (hasComments === undefined) { - hasComments = hasCategories; + + let maxTrashcanContents = options['maxTrashcanContents']; + if (hasTrashcan) { + if (maxTrashcanContents === undefined) { + maxTrashcanContents = 32; + } + } else { + maxTrashcanContents = 0; } - hasDisable = options['disable']; - if (hasDisable === undefined) { - hasDisable = hasCategories; + const rtl = !!options['rtl']; + let horizontalLayout = options['horizontalLayout']; + if (horizontalLayout === undefined) { + horizontalLayout = false; } - hasSounds = options['sounds']; - if (hasSounds === undefined) { - hasSounds = true; + let toolboxAtStart = options['toolboxPosition']; + toolboxAtStart = toolboxAtStart !== 'end'; + + /** @type {!toolbox.Position} */ + let toolboxPosition; + if (horizontalLayout) { + toolboxPosition = + toolboxAtStart ? toolbox.Position.TOP : toolbox.Position.BOTTOM; + } else { + toolboxPosition = (toolboxAtStart === rtl) ? toolbox.Position.RIGHT : + toolbox.Position.LEFT; } - } - let maxTrashcanContents = options['maxTrashcanContents']; - if (hasTrashcan) { - if (maxTrashcanContents === undefined) { - maxTrashcanContents = 32; + let hasCss = options['css']; + if (hasCss === undefined) { + hasCss = true; } - } else { - maxTrashcanContents = 0; - } - const rtl = !!options['rtl']; - let horizontalLayout = options['horizontalLayout']; - if (horizontalLayout === undefined) { - horizontalLayout = false; - } - let toolboxAtStart = options['toolboxPosition']; - toolboxAtStart = toolboxAtStart !== 'end'; + let pathToMedia = 'https://blockly-demo.appspot.com/static/media/'; + if (options['media']) { + pathToMedia = options['media']; + } else if (options['path']) { + // 'path' is a deprecated option which has been replaced by 'media'. + pathToMedia = options['path'] + 'media/'; + } + let oneBasedIndex; + if (options['oneBasedIndex'] === undefined) { + oneBasedIndex = true; + } else { + oneBasedIndex = !!options['oneBasedIndex']; + } + const renderer = options['renderer'] || 'geras'; - /** @type {!toolbox.Position} */ - let toolboxPosition; - if (horizontalLayout) { - toolboxPosition = - toolboxAtStart ? toolbox.Position.TOP : toolbox.Position.BOTTOM; - } else { - toolboxPosition = (toolboxAtStart === rtl) ? toolbox.Position.RIGHT : - toolbox.Position.LEFT; - } + const plugins = options['plugins'] || {}; - let hasCss = options['css']; - if (hasCss === undefined) { - hasCss = true; - } - let pathToMedia = 'https://blockly-demo.appspot.com/static/media/'; - if (options['media']) { - pathToMedia = options['media']; - } else if (options['path']) { - // 'path' is a deprecated option which has been replaced by 'media'. - pathToMedia = options['path'] + 'media/'; - } - let oneBasedIndex; - if (options['oneBasedIndex'] === undefined) { - oneBasedIndex = true; - } else { - oneBasedIndex = !!options['oneBasedIndex']; - } - const renderer = options['renderer'] || 'geras'; + /** @type {boolean} */ + this.RTL = rtl; + /** @type {boolean} */ + this.oneBasedIndex = oneBasedIndex; + /** @type {boolean} */ + this.collapse = hasCollapse; + /** @type {boolean} */ + this.comments = hasComments; + /** @type {boolean} */ + this.disable = hasDisable; + /** @type {boolean} */ + this.readOnly = readOnly; + /** @type {number} */ + this.maxBlocks = options['maxBlocks'] || Infinity; + /** @type {?Object} */ + this.maxInstances = options['maxInstances']; + /** @type {string} */ + this.pathToMedia = pathToMedia; + /** @type {boolean} */ + this.hasCategories = hasCategories; + /** @type {!Options.MoveOptions} */ + this.moveOptions = Options.parseMoveOptions_(options, hasCategories); + /** @deprecated January 2019 */ + this.hasScrollbars = !!this.moveOptions.scrollbars; + /** @type {boolean} */ + this.hasTrashcan = hasTrashcan; + /** @type {number} */ + this.maxTrashcanContents = maxTrashcanContents; + /** @type {boolean} */ + this.hasSounds = hasSounds; + /** @type {boolean} */ + this.hasCss = hasCss; + /** @type {boolean} */ + this.horizontalLayout = horizontalLayout; + /** @type {?toolbox.ToolboxInfo} */ + this.languageTree = toolboxJsonDef; + /** @type {!Options.GridOptions} */ + this.gridOptions = Options.parseGridOptions_(options); + /** @type {!Options.ZoomOptions} */ + this.zoomOptions = Options.parseZoomOptions_(options); + /** @type {!toolbox.Position} */ + this.toolboxPosition = toolboxPosition; + /** @type {!Theme} */ + this.theme = Options.parseThemeOptions_(options); + /** @type {string} */ + this.renderer = renderer; + /** @type {?Object} */ + this.rendererOverrides = options['rendererOverrides']; - const plugins = options['plugins'] || {}; + /** + * The SVG element for the grid pattern. + * Created during injection. + * @type {?SVGElement} + */ + this.gridPattern = null; - /** @type {boolean} */ - this.RTL = rtl; - /** @type {boolean} */ - this.oneBasedIndex = oneBasedIndex; - /** @type {boolean} */ - this.collapse = hasCollapse; - /** @type {boolean} */ - this.comments = hasComments; - /** @type {boolean} */ - this.disable = hasDisable; - /** @type {boolean} */ - this.readOnly = readOnly; - /** @type {number} */ - this.maxBlocks = options['maxBlocks'] || Infinity; - /** @type {?Object} */ - this.maxInstances = options['maxInstances']; - /** @type {string} */ - this.pathToMedia = pathToMedia; - /** @type {boolean} */ - this.hasCategories = hasCategories; - /** @type {!Options.MoveOptions} */ - this.moveOptions = Options.parseMoveOptions_(options, hasCategories); - /** @deprecated January 2019 */ - this.hasScrollbars = !!this.moveOptions.scrollbars; - /** @type {boolean} */ - this.hasTrashcan = hasTrashcan; - /** @type {number} */ - this.maxTrashcanContents = maxTrashcanContents; - /** @type {boolean} */ - this.hasSounds = hasSounds; - /** @type {boolean} */ - this.hasCss = hasCss; - /** @type {boolean} */ - this.horizontalLayout = horizontalLayout; - /** @type {?toolbox.ToolboxInfo} */ - this.languageTree = toolboxJsonDef; - /** @type {!Options.GridOptions} */ - this.gridOptions = Options.parseGridOptions_(options); - /** @type {!Options.ZoomOptions} */ - this.zoomOptions = Options.parseZoomOptions_(options); - /** @type {!toolbox.Position} */ - this.toolboxPosition = toolboxPosition; - /** @type {!Theme} */ - this.theme = Options.parseThemeOptions_(options); - /** @type {string} */ - this.renderer = renderer; - /** @type {?Object} */ - this.rendererOverrides = options['rendererOverrides']; + /** + * The parent of the current workspace, or null if there is no parent + * workspace. We can assert that this is of type WorkspaceSvg as opposed to + * Workspace as this is only used in a rendered workspace. + * @type {WorkspaceSvg} + */ + this.parentWorkspace = options['parentWorkspace']; - /** - * The SVG element for the grid pattern. - * Created during injection. - * @type {?SVGElement} - */ - this.gridPattern = null; + /** + * Map of plugin type to name of registered plugin or plugin class. + * @type {!Object} + */ + this.plugins = plugins; + + /** + * If set, sets the translation of the workspace to match the scrollbars. + * @type {undefined|function(!{x:number,y:number}):void} A function that sets + * the translation of the workspace to match the scrollbars. The argument + * Contains an x and/or y property which is a float between 0 and 1 + * specifying the degree of scrolling. + */ + this.setMetrics = undefined; + + /** + * @type {undefined|function():!Metrics} A function that returns a metrics + * object that describes the current workspace. + */ + this.getMetrics = undefined; + } /** - * The parent of the current workspace, or null if there is no parent - * workspace. We can assert that this is of type WorkspaceSvg as opposed to - * Workspace as this is only used in a rendered workspace. - * @type {WorkspaceSvg} + * Parse the user-specified move options, using reasonable defaults where + * behaviour is unspecified. + * @param {!Object} options Dictionary of options. + * @param {boolean} hasCategories Whether the workspace has categories or not. + * @return {!Options.MoveOptions} Normalized move options. + * @private */ - this.parentWorkspace = options['parentWorkspace']; + static parseMoveOptions_(options, hasCategories) { + const move = options['move'] || {}; + const moveOptions = {}; + if (move['scrollbars'] === undefined && options['scrollbars'] === undefined) { + moveOptions.scrollbars = hasCategories; + } else if (typeof move['scrollbars'] === 'object') { + moveOptions.scrollbars = {}; + moveOptions.scrollbars.horizontal = !!move['scrollbars']['horizontal']; + moveOptions.scrollbars.vertical = !!move['scrollbars']['vertical']; + // Convert scrollbars object to boolean if they have the same value. + // This allows us to easily check for whether any scrollbars exist using + // !!moveOptions.scrollbars. + if (moveOptions.scrollbars.horizontal && moveOptions.scrollbars.vertical) { + moveOptions.scrollbars = true; + } else if ( + !moveOptions.scrollbars.horizontal && + !moveOptions.scrollbars.vertical) { + moveOptions.scrollbars = false; + } + } else { + moveOptions.scrollbars = !!move['scrollbars'] || !!options['scrollbars']; + } + + if (!moveOptions.scrollbars || move['wheel'] === undefined) { + // Defaults to true if single-direction scroll is enabled. + moveOptions.wheel = typeof moveOptions.scrollbars === 'object'; + } else { + moveOptions.wheel = !!move['wheel']; + } + if (!moveOptions.scrollbars) { + moveOptions.drag = false; + } else if (move['drag'] === undefined) { + // Defaults to true if scrollbars is true. + moveOptions.drag = true; + } else { + moveOptions.drag = !!move['drag']; + } + return moveOptions; + } /** - * Map of plugin type to name of registered plugin or plugin class. - * @type {!Object} + * Parse the user-specified zoom options, using reasonable defaults where + * behaviour is unspecified. See zoom documentation: + * https://developers.google.com/blockly/guides/configure/web/zoom + * @param {!Object} options Dictionary of options. + * @return {!Options.ZoomOptions} Normalized zoom options. + * @private */ - this.plugins = plugins; + static parseZoomOptions_(options) { + const zoom = options['zoom'] || {}; + const zoomOptions = {}; + if (zoom['controls'] === undefined) { + zoomOptions.controls = false; + } else { + zoomOptions.controls = !!zoom['controls']; + } + if (zoom['wheel'] === undefined) { + zoomOptions.wheel = false; + } else { + zoomOptions.wheel = !!zoom['wheel']; + } + if (zoom['startScale'] === undefined) { + zoomOptions.startScale = 1; + } else { + zoomOptions.startScale = Number(zoom['startScale']); + } + if (zoom['maxScale'] === undefined) { + zoomOptions.maxScale = 3; + } else { + zoomOptions.maxScale = Number(zoom['maxScale']); + } + if (zoom['minScale'] === undefined) { + zoomOptions.minScale = 0.3; + } else { + zoomOptions.minScale = Number(zoom['minScale']); + } + if (zoom['scaleSpeed'] === undefined) { + zoomOptions.scaleSpeed = 1.2; + } else { + zoomOptions.scaleSpeed = Number(zoom['scaleSpeed']); + } + if (zoom['pinch'] === undefined) { + zoomOptions.pinch = zoomOptions.wheel || zoomOptions.controls; + } else { + zoomOptions.pinch = !!zoom['pinch']; + } + return zoomOptions; + } /** - * If set, sets the translation of the workspace to match the scrollbars. - * @type {undefined|function(!{x:number,y:number}):void} A function that sets - * the translation of the workspace to match the scrollbars. The argument - * Contains an x and/or y property which is a float between 0 and 1 - * specifying the degree of scrolling. + * Parse the user-specified grid options, using reasonable defaults where + * behaviour is unspecified. See grid documentation: + * https://developers.google.com/blockly/guides/configure/web/grid + * @param {!Object} options Dictionary of options. + * @return {!Options.GridOptions} Normalized grid options. + * @private */ - this.setMetrics = undefined; + static parseGridOptions_(options) { + const grid = options['grid'] || {}; + const gridOptions = {}; + gridOptions.spacing = Number(grid['spacing']) || 0; + gridOptions.colour = grid['colour'] || '#888'; + gridOptions.length = + (grid['length'] === undefined) ? 1 : Number(grid['length']); + gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap']; + return gridOptions; + } /** - * @type {undefined|function():!Metrics} A function that returns a metrics - * object that describes the current workspace. + * Parse the user-specified theme options, using the classic theme as a default. + * https://developers.google.com/blockly/guides/configure/web/themes + * @param {!Object} options Dictionary of options. + * @return {!Theme} A Blockly Theme. + * @private */ - this.getMetrics = undefined; + static parseThemeOptions_(options) { + const theme = options['theme'] || Classic; + if (typeof theme === 'string') { + return /** @type {!Theme} */ ( + registry.getObject(registry.Type.THEME, theme)); + } else if (theme instanceof Theme) { + return /** @type {!Theme} */ (theme); + } + return Theme.defineTheme( + theme.name || ('builtin' + idGenerator.getNextUniqueId()), theme); + } }; /** @@ -248,139 +386,4 @@ Options.ScrollbarOptions; */ Options.ZoomOptions; -/** - * Parse the user-specified move options, using reasonable defaults where - * behaviour is unspecified. - * @param {!Object} options Dictionary of options. - * @param {boolean} hasCategories Whether the workspace has categories or not. - * @return {!Options.MoveOptions} Normalized move options. - * @private - */ -Options.parseMoveOptions_ = function(options, hasCategories) { - const move = options['move'] || {}; - const moveOptions = {}; - if (move['scrollbars'] === undefined && options['scrollbars'] === undefined) { - moveOptions.scrollbars = hasCategories; - } else if (typeof move['scrollbars'] === 'object') { - moveOptions.scrollbars = {}; - moveOptions.scrollbars.horizontal = !!move['scrollbars']['horizontal']; - moveOptions.scrollbars.vertical = !!move['scrollbars']['vertical']; - // Convert scrollbars object to boolean if they have the same value. - // This allows us to easily check for whether any scrollbars exist using - // !!moveOptions.scrollbars. - if (moveOptions.scrollbars.horizontal && moveOptions.scrollbars.vertical) { - moveOptions.scrollbars = true; - } else if ( - !moveOptions.scrollbars.horizontal && - !moveOptions.scrollbars.vertical) { - moveOptions.scrollbars = false; - } - } else { - moveOptions.scrollbars = !!move['scrollbars'] || !!options['scrollbars']; - } - - if (!moveOptions.scrollbars || move['wheel'] === undefined) { - // Defaults to true if single-direction scroll is enabled. - moveOptions.wheel = typeof moveOptions.scrollbars === 'object'; - } else { - moveOptions.wheel = !!move['wheel']; - } - if (!moveOptions.scrollbars) { - moveOptions.drag = false; - } else if (move['drag'] === undefined) { - // Defaults to true if scrollbars is true. - moveOptions.drag = true; - } else { - moveOptions.drag = !!move['drag']; - } - return moveOptions; -}; - -/** - * Parse the user-specified zoom options, using reasonable defaults where - * behaviour is unspecified. See zoom documentation: - * https://developers.google.com/blockly/guides/configure/web/zoom - * @param {!Object} options Dictionary of options. - * @return {!Options.ZoomOptions} Normalized zoom options. - * @private - */ -Options.parseZoomOptions_ = function(options) { - const zoom = options['zoom'] || {}; - const zoomOptions = {}; - if (zoom['controls'] === undefined) { - zoomOptions.controls = false; - } else { - zoomOptions.controls = !!zoom['controls']; - } - if (zoom['wheel'] === undefined) { - zoomOptions.wheel = false; - } else { - zoomOptions.wheel = !!zoom['wheel']; - } - if (zoom['startScale'] === undefined) { - zoomOptions.startScale = 1; - } else { - zoomOptions.startScale = Number(zoom['startScale']); - } - if (zoom['maxScale'] === undefined) { - zoomOptions.maxScale = 3; - } else { - zoomOptions.maxScale = Number(zoom['maxScale']); - } - if (zoom['minScale'] === undefined) { - zoomOptions.minScale = 0.3; - } else { - zoomOptions.minScale = Number(zoom['minScale']); - } - if (zoom['scaleSpeed'] === undefined) { - zoomOptions.scaleSpeed = 1.2; - } else { - zoomOptions.scaleSpeed = Number(zoom['scaleSpeed']); - } - if (zoom['pinch'] === undefined) { - zoomOptions.pinch = zoomOptions.wheel || zoomOptions.controls; - } else { - zoomOptions.pinch = !!zoom['pinch']; - } - return zoomOptions; -}; - -/** - * Parse the user-specified grid options, using reasonable defaults where - * behaviour is unspecified. See grid documentation: - * https://developers.google.com/blockly/guides/configure/web/grid - * @param {!Object} options Dictionary of options. - * @return {!Options.GridOptions} Normalized grid options. - * @private - */ -Options.parseGridOptions_ = function(options) { - const grid = options['grid'] || {}; - const gridOptions = {}; - gridOptions.spacing = Number(grid['spacing']) || 0; - gridOptions.colour = grid['colour'] || '#888'; - gridOptions.length = - (grid['length'] === undefined) ? 1 : Number(grid['length']); - gridOptions.snap = gridOptions.spacing > 0 && !!grid['snap']; - return gridOptions; -}; - -/** - * Parse the user-specified theme options, using the classic theme as a default. - * https://developers.google.com/blockly/guides/configure/web/themes - * @param {!Object} options Dictionary of options. - * @return {!Theme} A Blockly Theme. - * @private - */ -Options.parseThemeOptions_ = function(options) { - const theme = options['theme'] || Classic; - if (typeof theme === 'string') { - return /** @type {!Theme} */ ( - registry.getObject(registry.Type.THEME, theme)); - } else if (theme instanceof Theme) { - return /** @type {!Theme} */ (theme); - } - return Theme.defineTheme( - theme.name || ('builtin' + idGenerator.getNextUniqueId()), theme); -}; - exports.Options = Options; From 51c39568c3f260c5b22607e57b6334a32c06971e Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 10 Feb 2022 12:06:41 -0800 Subject: [PATCH 3/9] refactor: convert generator.js to es6 class --- core/generator.js | 900 +++++++++++++++++++++++----------------------- core/options.js | 2 +- 2 files changed, 454 insertions(+), 448 deletions(-) diff --git a/core/generator.js b/core/generator.js index 15be91bd40f..f3df65a1585 100644 --- a/core/generator.js +++ b/core/generator.js @@ -29,391 +29,506 @@ const {Workspace} = goog.requireType('Blockly.Workspace'); /** * Class for a code generator that translates the blocks into a language. - * @param {string} name Language name of this generator. - * @constructor - * @alias Blockly.Generator + * @unrestricted */ -const Generator = function(name) { - this.name_ = name; - this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ = - new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g'); - +class Generator { /** - * Arbitrary code to inject into locations that risk causing infinite loops. - * Any instances of '%1' will be replaced by the block ID that failed. - * E.g. ' checkTimeout(%1);\n' - * @type {?string} + * @param {string} name Language name of this generator. + * @alias Blockly.Generator */ - this.INFINITE_LOOP_TRAP = null; + constructor(name) { + this.name_ = name; - /** - * Arbitrary code to inject before every statement. - * Any instances of '%1' will be replaced by the block ID of the statement. - * E.g. 'highlight(%1);\n' - * @type {?string} - */ - this.STATEMENT_PREFIX = null; + /** + * This is used as a placeholder in functions defined using + * Generator.provideFunction_. It must not be legal code that could + * legitimately appear in a function definition (or comment), and it must + * not confuse the regular expression parser. + * @type {string} + * @protected + */ + this.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; - /** - * Arbitrary code to inject after every statement. - * Any instances of '%1' will be replaced by the block ID of the statement. - * E.g. 'highlight(%1);\n' - * @type {?string} - */ - this.STATEMENT_SUFFIX = null; + this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ = + new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g'); + + /** + * Arbitrary code to inject into locations that risk causing infinite loops. + * Any instances of '%1' will be replaced by the block ID that failed. + * E.g. ' checkTimeout(%1);\n' + * @type {?string} + */ + this.INFINITE_LOOP_TRAP = null; + + /** + * Arbitrary code to inject before every statement. + * Any instances of '%1' will be replaced by the block ID of the statement. + * E.g. 'highlight(%1);\n' + * @type {?string} + */ + this.STATEMENT_PREFIX = null; + + /** + * Arbitrary code to inject after every statement. + * Any instances of '%1' will be replaced by the block ID of the statement. + * E.g. 'highlight(%1);\n' + * @type {?string} + */ + this.STATEMENT_SUFFIX = null; + + /** + * The method of indenting. Defaults to two spaces, but language generators + * may override this to increase indent or change to tabs. + * @type {string} + */ + this.INDENT = ' '; + + /** + * Maximum length for a comment before wrapping. Does not account for + * indenting level. + * @type {number} + */ + this.COMMENT_WRAP = 60; + + /** + * List of outer-inner pairings that do NOT require parentheses. + * @type {!Array>} + */ + this.ORDER_OVERRIDES = []; + + /** + * Whether the init method has been called. + * Generators that set this flag to false after creation and true in init + * will cause blockToCode to emit a warning if the generator has not been + * initialized. If this flag is untouched, it will have no effect. + * @type {?boolean} + */ + this.isInitialized = null; + + /** + * Comma-separated list of reserved words. + * @type {string} + * @protected + */ + this.RESERVED_WORDS_ = ''; + + /** + * A dictionary of definitions to be printed before the code. + * @type {!Object|undefined} + * @protected + */ + this.definitions_ = undefined; + + /** + * A dictionary mapping desired function names in definitions_ to actual + * function names (to avoid collisions with user functions). + * @type {!Object|undefined} + * @protected + */ + this.functionNames_ = undefined; + + /** + * A database of variable and procedure names. + * @type {!Names|undefined} + * @protected + */ + this.nameDB_ = undefined; + } /** - * The method of indenting. Defaults to two spaces, but language generators - * may override this to increase indent or change to tabs. - * @type {string} + * Generate code for all blocks in the workspace to the specified language. + * @param {!Workspace=} workspace Workspace to generate code from. + * @return {string} Generated code. */ - this.INDENT = ' '; + workspaceToCode(workspace) { + if (!workspace) { + // Backwards compatibility from before there could be multiple workspaces. + console.warn('No workspace specified in workspaceToCode call. Guessing.'); + workspace = common.getMainWorkspace(); + } + let code = []; + this.init(workspace); + const blocks = workspace.getTopBlocks(true); + for (let i = 0, block; (block = blocks[i]); i++) { + let line = this.blockToCode(block); + if (Array.isArray(line)) { + // Value blocks return tuples of code and operator order. + // Top-level blocks don't care about operator order. + line = line[0]; + } + if (line) { + if (block.outputConnection) { + // This block is a naked value. Ask the language's code generator if + // it wants to append a semicolon, or something. + line = this.scrubNakedValue(line); + if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { + line = this.injectId(this.STATEMENT_PREFIX, block) + line; + } + if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) { + line = line + this.injectId(this.STATEMENT_SUFFIX, block); + } + } + code.push(line); + } + } + code = code.join('\n'); // Blank line between each section. + code = this.finish(code); + // Final scrubbing of whitespace. + code = code.replace(/^\s+\n/, ''); + code = code.replace(/\n\s+$/, '\n'); + code = code.replace(/[ \t]+\n/g, '\n'); + return code; + } + + // The following are some helpful functions which can be used by multiple + + // languages. /** - * Maximum length for a comment before wrapping. Does not account for - * indenting level. - * @type {number} + * Prepend a common prefix onto each line of code. + * Intended for indenting code or adding comment markers. + * @param {string} text The lines of code. + * @param {string} prefix The common prefix. + * @return {string} The prefixed lines of code. */ - this.COMMENT_WRAP = 60; + prefixLines(text, prefix) { + return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix); + } /** - * List of outer-inner pairings that do NOT require parentheses. - * @type {!Array>} + * Recursively spider a tree of blocks, returning all their comments. + * @param {!Block} block The block from which to start spidering. + * @return {string} Concatenated list of comments. */ - this.ORDER_OVERRIDES = []; + allNestedComments(block) { + const comments = []; + const blocks = block.getDescendants(true); + for (let i = 0; i < blocks.length; i++) { + const comment = blocks[i].getCommentText(); + if (comment) { + comments.push(comment); + } + } + // Append an empty string to create a trailing line break when joined. + if (comments.length) { + comments.push(''); + } + return comments.join('\n'); + } /** - * Whether the init method has been called. - * Generators that set this flag to false after creation and true in init - * will cause blockToCode to emit a warning if the generator has not been - * initialized. If this flag is untouched, it will have no effect. - * @type {?boolean} + * Generate code for the specified block (and attached blocks). + * The generator must be initialized before calling this function. + * @param {Block} block The block to generate code for. + * @param {boolean=} opt_thisOnly True to generate code for only this statement. + * @return {string|!Array} For statement blocks, the generated code. + * For value blocks, an array containing the generated code and an + * operator order value. Returns '' if block is null. */ - this.isInitialized = null; -}; + blockToCode(block, opt_thisOnly) { + if (this.isInitialized === false) { + console.warn( + 'Generator init was not called before blockToCode was called.'); + } + if (!block) { + return ''; + } + if (!block.isEnabled()) { + // Skip past this block if it is disabled. + return opt_thisOnly ? '' : this.blockToCode(block.getNextBlock()); + } + if (block.isInsertionMarker()) { + // Skip past insertion markers. + return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]); + } -/** - * Generate code for all blocks in the workspace to the specified language. - * @param {!Workspace=} workspace Workspace to generate code from. - * @return {string} Generated code. - */ -Generator.prototype.workspaceToCode = function(workspace) { - if (!workspace) { - // Backwards compatibility from before there could be multiple workspaces. - console.warn('No workspace specified in workspaceToCode call. Guessing.'); - workspace = common.getMainWorkspace(); - } - let code = []; - this.init(workspace); - const blocks = workspace.getTopBlocks(true); - for (let i = 0, block; (block = blocks[i]); i++) { - let line = this.blockToCode(block); - if (Array.isArray(line)) { - // Value blocks return tuples of code and operator order. - // Top-level blocks don't care about operator order. - line = line[0]; + const func = this[block.type]; + if (typeof func !== 'function') { + throw Error( + 'Language "' + this.name_ + '" does not know how to generate ' + + 'code for block type "' + block.type + '".'); } - if (line) { - if (block.outputConnection) { - // This block is a naked value. Ask the language's code generator if - // it wants to append a semicolon, or something. - line = this.scrubNakedValue(line); - if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { - line = this.injectId(this.STATEMENT_PREFIX, block) + line; - } - if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) { - line = line + this.injectId(this.STATEMENT_SUFFIX, block); - } + // First argument to func.call is the value of 'this' in the generator. + // Prior to 24 September 2013 'this' was the only way to access the block. + // The current preferred method of accessing the block is through the second + // argument to func.call, which becomes the first parameter to the generator. + let code = func.call(block, block); + if (Array.isArray(code)) { + // Value blocks return tuples of code and operator order. + if (!block.outputConnection) { + throw TypeError('Expecting string from statement block: ' + block.type); + } + return [this.scrub_(block, code[0], opt_thisOnly), code[1]]; + } else if (typeof code === 'string') { + if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { + code = this.injectId(this.STATEMENT_PREFIX, block) + code; } - code.push(line); + if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) { + code = code + this.injectId(this.STATEMENT_SUFFIX, block); + } + return this.scrub_(block, code, opt_thisOnly); + } else if (code === null) { + // Block has handled code generation itself. + return ''; } + throw SyntaxError('Invalid code generated: ' + code); } - code = code.join('\n'); // Blank line between each section. - code = this.finish(code); - // Final scrubbing of whitespace. - code = code.replace(/^\s+\n/, ''); - code = code.replace(/\n\s+$/, '\n'); - code = code.replace(/[ \t]+\n/g, '\n'); - return code; -}; - -// The following are some helpful functions which can be used by multiple -// languages. -/** - * Prepend a common prefix onto each line of code. - * Intended for indenting code or adding comment markers. - * @param {string} text The lines of code. - * @param {string} prefix The common prefix. - * @return {string} The prefixed lines of code. - */ -Generator.prototype.prefixLines = function(text, prefix) { - return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix); -}; + /** + * Generate code representing the specified value input. + * @param {!Block} block The block containing the input. + * @param {string} name The name of the input. + * @param {number} outerOrder The maximum binding strength (minimum order value) + * of any operators adjacent to "block". + * @return {string} Generated code or '' if no blocks are connected or the + * specified input does not exist. + */ + valueToCode(block, name, outerOrder) { + if (isNaN(outerOrder)) { + throw TypeError('Expecting valid order from block: ' + block.type); + } + const targetBlock = block.getInputTargetBlock(name); + if (!targetBlock) { + return ''; + } + const tuple = this.blockToCode(targetBlock); + if (tuple === '') { + // Disabled block. + return ''; + } + // Value blocks must return code and order of operations info. + // Statement blocks must only return code. + if (!Array.isArray(tuple)) { + throw TypeError('Expecting tuple from value block: ' + targetBlock.type); + } + let code = tuple[0]; + const innerOrder = tuple[1]; + if (isNaN(innerOrder)) { + throw TypeError( + 'Expecting valid order from value block: ' + targetBlock.type); + } + if (!code) { + return ''; + } -/** - * Recursively spider a tree of blocks, returning all their comments. - * @param {!Block} block The block from which to start spidering. - * @return {string} Concatenated list of comments. - */ -Generator.prototype.allNestedComments = function(block) { - const comments = []; - const blocks = block.getDescendants(true); - for (let i = 0; i < blocks.length; i++) { - const comment = blocks[i].getCommentText(); - if (comment) { - comments.push(comment); + // Add parentheses if needed. + let parensNeeded = false; + const outerOrderClass = Math.floor(outerOrder); + const innerOrderClass = Math.floor(innerOrder); + if (outerOrderClass <= innerOrderClass) { + if (outerOrderClass === innerOrderClass && + (outerOrderClass === 0 || outerOrderClass === 99)) { + // Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs. + // 0 is the atomic order, 99 is the none order. No parentheses needed. + // In all known languages multiple such code blocks are not order + // sensitive. In fact in Python ('a' 'b') 'c' would fail. + } else { + // The operators outside this code are stronger than the operators + // inside this code. To prevent the code from being pulled apart, + // wrap the code in parentheses. + parensNeeded = true; + // Check for special exceptions. + for (let i = 0; i < this.ORDER_OVERRIDES.length; i++) { + if (this.ORDER_OVERRIDES[i][0] === outerOrder && + this.ORDER_OVERRIDES[i][1] === innerOrder) { + parensNeeded = false; + break; + } + } + } } + if (parensNeeded) { + // Technically, this should be handled on a language-by-language basis. + // However all known (sane) languages use parentheses for grouping. + code = '(' + code + ')'; + } + return code; } - // Append an empty string to create a trailing line break when joined. - if (comments.length) { - comments.push(''); - } - return comments.join('\n'); -}; -/** - * Generate code for the specified block (and attached blocks). - * The generator must be initialized before calling this function. - * @param {Block} block The block to generate code for. - * @param {boolean=} opt_thisOnly True to generate code for only this statement. - * @return {string|!Array} For statement blocks, the generated code. - * For value blocks, an array containing the generated code and an - * operator order value. Returns '' if block is null. - */ -Generator.prototype.blockToCode = function(block, opt_thisOnly) { - if (this.isInitialized === false) { - console.warn( - 'Generator init was not called before blockToCode was called.'); - } - if (!block) { - return ''; - } - if (!block.isEnabled()) { - // Skip past this block if it is disabled. - return opt_thisOnly ? '' : this.blockToCode(block.getNextBlock()); - } - if (block.isInsertionMarker()) { - // Skip past insertion markers. - return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]); + /** + * Generate a code string representing the blocks attached to the named + * statement input. Indent the code. + * This is mainly used in generators. When trying to generate code to evaluate + * look at using workspaceToCode or blockToCode. + * @param {!Block} block The block containing the input. + * @param {string} name The name of the input. + * @return {string} Generated code or '' if no blocks are connected. + */ + statementToCode(block, name) { + const targetBlock = block.getInputTargetBlock(name); + let code = this.blockToCode(targetBlock); + // Value blocks must return code and order of operations info. + // Statement blocks must only return code. + if (typeof code !== 'string') { + throw TypeError( + 'Expecting code from statement block: ' + + (targetBlock && targetBlock.type)); + } + if (code) { + code = this.prefixLines(/** @type {string} */ (code), this.INDENT); + } + return code; } - const func = this[block.type]; - if (typeof func !== 'function') { - throw Error( - 'Language "' + this.name_ + '" does not know how to generate ' + - 'code for block type "' + block.type + '".'); - } - // First argument to func.call is the value of 'this' in the generator. - // Prior to 24 September 2013 'this' was the only way to access the block. - // The current preferred method of accessing the block is through the second - // argument to func.call, which becomes the first parameter to the generator. - let code = func.call(block, block); - if (Array.isArray(code)) { - // Value blocks return tuples of code and operator order. - if (!block.outputConnection) { - throw TypeError('Expecting string from statement block: ' + block.type); - } - return [this.scrub_(block, code[0], opt_thisOnly), code[1]]; - } else if (typeof code === 'string') { - if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { - code = this.injectId(this.STATEMENT_PREFIX, block) + code; + /** + * Add an infinite loop trap to the contents of a loop. + * Add statement suffix at the start of the loop block (right after the loop + * statement executes), and a statement prefix to the end of the loop block + * (right before the loop statement executes). + * @param {string} branch Code for loop contents. + * @param {!Block} block Enclosing block. + * @return {string} Loop contents, with infinite loop trap added. + */ + addLoopTrap(branch, block) { + if (this.INFINITE_LOOP_TRAP) { + branch = this.prefixLines( + this.injectId(this.INFINITE_LOOP_TRAP, block), this.INDENT) + + branch; } if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) { - code = code + this.injectId(this.STATEMENT_SUFFIX, block); + branch = this.prefixLines( + this.injectId(this.STATEMENT_SUFFIX, block), this.INDENT) + + branch; } - return this.scrub_(block, code, opt_thisOnly); - } else if (code === null) { - // Block has handled code generation itself. - return ''; + if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { + branch = branch + + this.prefixLines( + this.injectId(this.STATEMENT_PREFIX, block), this.INDENT); + } + return branch; } - throw SyntaxError('Invalid code generated: ' + code); -}; -/** - * Generate code representing the specified value input. - * @param {!Block} block The block containing the input. - * @param {string} name The name of the input. - * @param {number} outerOrder The maximum binding strength (minimum order value) - * of any operators adjacent to "block". - * @return {string} Generated code or '' if no blocks are connected or the - * specified input does not exist. - */ -Generator.prototype.valueToCode = function(block, name, outerOrder) { - if (isNaN(outerOrder)) { - throw TypeError('Expecting valid order from block: ' + block.type); - } - const targetBlock = block.getInputTargetBlock(name); - if (!targetBlock) { - return ''; - } - const tuple = this.blockToCode(targetBlock); - if (tuple === '') { - // Disabled block. - return ''; - } - // Value blocks must return code and order of operations info. - // Statement blocks must only return code. - if (!Array.isArray(tuple)) { - throw TypeError('Expecting tuple from value block: ' + targetBlock.type); - } - let code = tuple[0]; - const innerOrder = tuple[1]; - if (isNaN(innerOrder)) { - throw TypeError( - 'Expecting valid order from value block: ' + targetBlock.type); + /** + * Inject a block ID into a message to replace '%1'. + * Used for STATEMENT_PREFIX, STATEMENT_SUFFIX, and INFINITE_LOOP_TRAP. + * @param {string} msg Code snippet with '%1'. + * @param {!Block} block Block which has an ID. + * @return {string} Code snippet with ID. + */ + injectId(msg, block) { + const id = block.id.replace(/\$/g, '$$$$'); // Issue 251. + return msg.replace(/%1/g, '\'' + id + '\''); } - if (!code) { - return ''; + + /** + * Add one or more words to the list of reserved words for this language. + * @param {string} words Comma-separated list of words to add to the list. + * No spaces. Duplicates are ok. + */ + addReservedWords(words) { + this.RESERVED_WORDS_ += words + ','; } - // Add parentheses if needed. - let parensNeeded = false; - const outerOrderClass = Math.floor(outerOrder); - const innerOrderClass = Math.floor(innerOrder); - if (outerOrderClass <= innerOrderClass) { - if (outerOrderClass === innerOrderClass && - (outerOrderClass === 0 || outerOrderClass === 99)) { - // Don't generate parens around NONE-NONE and ATOMIC-ATOMIC pairs. - // 0 is the atomic order, 99 is the none order. No parentheses needed. - // In all known languages multiple such code blocks are not order - // sensitive. In fact in Python ('a' 'b') 'c' would fail. - } else { - // The operators outside this code are stronger than the operators - // inside this code. To prevent the code from being pulled apart, - // wrap the code in parentheses. - parensNeeded = true; - // Check for special exceptions. - for (let i = 0; i < this.ORDER_OVERRIDES.length; i++) { - if (this.ORDER_OVERRIDES[i][0] === outerOrder && - this.ORDER_OVERRIDES[i][1] === innerOrder) { - parensNeeded = false; - break; - } + /** + * Define a developer-defined function (not a user-defined procedure) to be + * included in the generated code. Used for creating private helper functions. + * The first time this is called with a given desiredName, the code is + * saved and an actual name is generated. Subsequent calls with the + * same desiredName have no effect but have the same return value. + * + * It is up to the caller to make sure the same desiredName is not + * used for different helper functions (e.g. use "colourRandom" and + * "listRandom", not "random"). There is no danger of colliding with reserved + * words, or user-defined variable or procedure names. + * + * The code gets output when Generator.finish() is called. + * + * @param {string} desiredName The desired name of the function + * (e.g. mathIsPrime). + * @param {!Array|string} code A list of statements or one multi-line + * code string. Use ' ' for indents (they will be replaced). + * @return {string} The actual name of the new function. This may differ + * from desiredName if the former has already been taken by the user. + * @protected + */ + provideFunction_(desiredName, code) { + if (!this.definitions_[desiredName]) { + const functionName = + this.nameDB_.getDistinctName(desiredName, NameType.PROCEDURE); + this.functionNames_[desiredName] = functionName; + if (Array.isArray(code)) { + code = code.join('\n'); } + let codeText = code.trim().replace( + this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName); + // Change all ' ' indents into the desired indent. + // To avoid an infinite loop of replacements, change all indents to '\0' + // character first, then replace them all with the indent. + // We are assuming that no provided functions contain a literal null char. + let oldCodeText; + while (oldCodeText !== codeText) { + oldCodeText = codeText; + codeText = codeText.replace(/^(( {2})*) {2}/gm, '$1\0'); + } + codeText = codeText.replace(/\0/g, this.INDENT); + this.definitions_[desiredName] = codeText; } + return this.functionNames_[desiredName]; } - if (parensNeeded) { - // Technically, this should be handled on a language-by-language basis. - // However all known (sane) languages use parentheses for grouping. - code = '(' + code + ')'; - } - return code; -}; -/** - * Generate a code string representing the blocks attached to the named - * statement input. Indent the code. - * This is mainly used in generators. When trying to generate code to evaluate - * look at using workspaceToCode or blockToCode. - * @param {!Block} block The block containing the input. - * @param {string} name The name of the input. - * @return {string} Generated code or '' if no blocks are connected. - */ -Generator.prototype.statementToCode = function(block, name) { - const targetBlock = block.getInputTargetBlock(name); - let code = this.blockToCode(targetBlock); - // Value blocks must return code and order of operations info. - // Statement blocks must only return code. - if (typeof code !== 'string') { - throw TypeError( - 'Expecting code from statement block: ' + - (targetBlock && targetBlock.type)); - } - if (code) { - code = this.prefixLines(/** @type {string} */ (code), this.INDENT); + /** + * Hook for code to run before code generation starts. + * Subclasses may override this, e.g. to initialise the database of variable + * names. + * @param {!Workspace} _workspace Workspace to generate code from. + */ + init(_workspace) { + // Optionally override + // Create a dictionary of definitions to be printed before the code. + this.definitions_ = Object.create(null); + // Create a dictionary mapping desired developer-defined function names in + // definitions_ to actual function names (to avoid collisions with + // user-defined procedures). + this.functionNames_ = Object.create(null); } - return code; -}; -/** - * Add an infinite loop trap to the contents of a loop. - * Add statement suffix at the start of the loop block (right after the loop - * statement executes), and a statement prefix to the end of the loop block - * (right before the loop statement executes). - * @param {string} branch Code for loop contents. - * @param {!Block} block Enclosing block. - * @return {string} Loop contents, with infinite loop trap added. - */ -Generator.prototype.addLoopTrap = function(branch, block) { - if (this.INFINITE_LOOP_TRAP) { - branch = this.prefixLines( - this.injectId(this.INFINITE_LOOP_TRAP, block), this.INDENT) + - branch; - } - if (this.STATEMENT_SUFFIX && !block.suppressPrefixSuffix) { - branch = this.prefixLines( - this.injectId(this.STATEMENT_SUFFIX, block), this.INDENT) + - branch; - } - if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) { - branch = branch + - this.prefixLines( - this.injectId(this.STATEMENT_PREFIX, block), this.INDENT); + /** + * Common tasks for generating code from blocks. This is called from + * blockToCode and is called on every block, not just top level blocks. + * Subclasses may override this, e.g. to generate code for statements following + * the block, or to handle comments for the specified block and any connected + * value blocks. + * @param {!Block} _block The current block. + * @param {string} code The code created for this block. + * @param {boolean=} _opt_thisOnly True to generate code for only this + * statement. + * @return {string} Code with comments and subsequent blocks added. + * @protected + */ + scrub_(_block, code, _opt_thisOnly) { + // Optionally override + return code; } - return branch; -}; - -/** - * Inject a block ID into a message to replace '%1'. - * Used for STATEMENT_PREFIX, STATEMENT_SUFFIX, and INFINITE_LOOP_TRAP. - * @param {string} msg Code snippet with '%1'. - * @param {!Block} block Block which has an ID. - * @return {string} Code snippet with ID. - */ -Generator.prototype.injectId = function(msg, block) { - const id = block.id.replace(/\$/g, '$$$$'); // Issue 251. - return msg.replace(/%1/g, '\'' + id + '\''); -}; -/** - * Comma-separated list of reserved words. - * @type {string} - * @protected - */ -Generator.prototype.RESERVED_WORDS_ = ''; + /** + * Hook for code to run at end of code generation. + * Subclasses may override this, e.g. to prepend the generated code with import + * statements or variable definitions. + * @param {string} code Generated code. + * @return {string} Completed code. + */ + finish(code) { + // Optionally override + // Clean up temporary data. + delete this.definitions_; + delete this.functionNames_; + return code; + } -/** - * Add one or more words to the list of reserved words for this language. - * @param {string} words Comma-separated list of words to add to the list. - * No spaces. Duplicates are ok. - */ -Generator.prototype.addReservedWords = function(words) { - this.RESERVED_WORDS_ += words + ','; + /** + * Naked values are top-level blocks with outputs that aren't plugged into + * anything. + * Subclasses may override this, e.g. if their language does not allow + * naked values. + * @param {string} line Line of generated code. + * @return {string} Legal line of code. + */ + scrubNakedValue(line) { + // Optionally override + return line; + } }; -/** - * This is used as a placeholder in functions defined using - * Generator.provideFunction_. It must not be legal code that could - * legitimately appear in a function definition (or comment), and it must - * not confuse the regular expression parser. - * @type {string} - * @protected - */ -Generator.prototype.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}'; - -/** - * A dictionary of definitions to be printed before the code. - * @type {!Object|undefined} - * @protected - */ -Generator.prototype.definitions_; - -/** - * A dictionary mapping desired function names in definitions_ to actual - * function names (to avoid collisions with user functions). - * @type {!Object|undefined} - * @protected - */ -Generator.prototype.functionNames_; - -/** - * A database of variable and procedure names. - * @type {!Names|undefined} - * @protected - */ -Generator.prototype.nameDB_; - Object.defineProperties(Generator.prototype, { /** * A database of variable names. @@ -441,115 +556,6 @@ Object.defineProperties(Generator.prototype, { this.nameDB_ = nameDb; }, }, -}); - -/** - * Define a developer-defined function (not a user-defined procedure) to be - * included in the generated code. Used for creating private helper functions. - * The first time this is called with a given desiredName, the code is - * saved and an actual name is generated. Subsequent calls with the - * same desiredName have no effect but have the same return value. - * - * It is up to the caller to make sure the same desiredName is not - * used for different helper functions (e.g. use "colourRandom" and - * "listRandom", not "random"). There is no danger of colliding with reserved - * words, or user-defined variable or procedure names. - * - * The code gets output when Generator.finish() is called. - * - * @param {string} desiredName The desired name of the function - * (e.g. mathIsPrime). - * @param {!Array|string} code A list of statements or one multi-line - * code string. Use ' ' for indents (they will be replaced). - * @return {string} The actual name of the new function. This may differ - * from desiredName if the former has already been taken by the user. - * @protected - */ -Generator.prototype.provideFunction_ = function(desiredName, code) { - if (!this.definitions_[desiredName]) { - const functionName = - this.nameDB_.getDistinctName(desiredName, NameType.PROCEDURE); - this.functionNames_[desiredName] = functionName; - if (Array.isArray(code)) { - code = code.join('\n'); - } - let codeText = code.trim().replace( - this.FUNCTION_NAME_PLACEHOLDER_REGEXP_, functionName); - // Change all ' ' indents into the desired indent. - // To avoid an infinite loop of replacements, change all indents to '\0' - // character first, then replace them all with the indent. - // We are assuming that no provided functions contain a literal null char. - let oldCodeText; - while (oldCodeText !== codeText) { - oldCodeText = codeText; - codeText = codeText.replace(/^(( {2})*) {2}/gm, '$1\0'); - } - codeText = codeText.replace(/\0/g, this.INDENT); - this.definitions_[desiredName] = codeText; - } - return this.functionNames_[desiredName]; -}; - -/** - * Hook for code to run before code generation starts. - * Subclasses may override this, e.g. to initialise the database of variable - * names. - * @param {!Workspace} _workspace Workspace to generate code from. - */ -Generator.prototype.init = function(_workspace) { - // Optionally override - // Create a dictionary of definitions to be printed before the code. - this.definitions_ = Object.create(null); - // Create a dictionary mapping desired developer-defined function names in - // definitions_ to actual function names (to avoid collisions with - // user-defined procedures). - this.functionNames_ = Object.create(null); -}; - -/** - * Common tasks for generating code from blocks. This is called from - * blockToCode and is called on every block, not just top level blocks. - * Subclasses may override this, e.g. to generate code for statements following - * the block, or to handle comments for the specified block and any connected - * value blocks. - * @param {!Block} _block The current block. - * @param {string} code The code created for this block. - * @param {boolean=} _opt_thisOnly True to generate code for only this - * statement. - * @return {string} Code with comments and subsequent blocks added. - * @protected - */ -Generator.prototype.scrub_ = function(_block, code, _opt_thisOnly) { - // Optionally override - return code; -}; - -/** - * Hook for code to run at end of code generation. - * Subclasses may override this, e.g. to prepend the generated code with import - * statements or variable definitions. - * @param {string} code Generated code. - * @return {string} Completed code. - */ -Generator.prototype.finish = function(code) { - // Optionally override - // Clean up temporary data. - delete this.definitions_; - delete this.functionNames_; - return code; -}; - -/** - * Naked values are top-level blocks with outputs that aren't plugged into - * anything. - * Subclasses may override this, e.g. if their language does not allow - * naked values. - * @param {string} line Line of generated code. - * @return {string} Legal line of code. - */ -Generator.prototype.scrubNakedValue = function(line) { - // Optionally override - return line; -}; +}) exports.Generator = Generator; diff --git a/core/options.js b/core/options.js index fb28ec919f3..78ebeaf24ff 100644 --- a/core/options.js +++ b/core/options.js @@ -340,7 +340,7 @@ class Options { return Theme.defineTheme( theme.name || ('builtin' + idGenerator.getNextUniqueId()), theme); } -}; +} /** * Grid Options. From f741d2176a2aa58ddcf983ba22dc2c81ed2feac5 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 10 Feb 2022 12:10:54 -0800 Subject: [PATCH 4/9] refactor: convert workspace_events.js to es6 class --- core/events/workspace_events.js | 102 +++++++++++++++----------------- 1 file changed, 48 insertions(+), 54 deletions(-) diff --git a/core/events/workspace_events.js b/core/events/workspace_events.js index d240f3d93e9..7d34de0871f 100644 --- a/core/events/workspace_events.js +++ b/core/events/workspace_events.js @@ -16,7 +16,6 @@ goog.module('Blockly.Events.FinishedLoading'); const eventUtils = goog.require('Blockly.Events.utils'); -const object = goog.require('Blockly.utils.object'); const registry = goog.require('Blockly.registry'); const {Abstract} = goog.require('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ @@ -28,70 +27,65 @@ const {Workspace} = goog.requireType('Blockly.Workspace'); * Used to notify the developer when the workspace has finished loading (i.e * domToWorkspace). * Finished loading events do not record undo or redo. - * @param {!Workspace=} opt_workspace The workspace that has finished - * loading. Undefined for a blank event. * @extends {Abstract} - * @constructor - * @alias Blockly.Events.FinishedLoading */ -const FinishedLoading = function(opt_workspace) { +class FinishedLoading extends Abstract { /** - * Whether or not the event is blank (to be populated by fromJson). - * @type {boolean} + * @param {!Workspace=} opt_workspace The workspace that has finished + * loading. Undefined for a blank event. + * @alias Blockly.Events.FinishedLoading */ - this.isBlank = typeof opt_workspace === 'undefined'; + constructor(opt_workspace) { + super(); + /** + * Whether or not the event is blank (to be populated by fromJson). + * @type {boolean} + */ + this.isBlank = typeof opt_workspace === 'undefined'; + + /** + * The workspace identifier for this event. + * @type {string} + */ + this.workspaceId = opt_workspace ? opt_workspace.id : ''; + + // Workspace events do not undo or redo. + this.recordUndo = false; + + /** + * Type of this event. + * @type {string} + */ + this.type = eventUtils.FINISHED_LOADING; + } /** - * The workspace identifier for this event. - * @type {string} + * Encode the event as JSON. + * @return {!Object} JSON representation. */ - this.workspaceId = opt_workspace ? opt_workspace.id : ''; + toJson() { + const json = { + 'type': this.type, + }; + if (this.group) { + json['group'] = this.group; + } + if (this.workspaceId) { + json['workspaceId'] = this.workspaceId; + } + return json; + } /** - * The event group ID for the group this event belongs to. Groups define - * events that should be treated as an single action from the user's - * perspective, and should be undone together. - * @type {string} + * Decode the JSON event. + * @param {!Object} json JSON representation. */ - this.group = eventUtils.getGroup(); - - // Workspace events do not undo or redo. - this.recordUndo = false; -}; -object.inherits(FinishedLoading, Abstract); - -/** - * Type of this event. - * @type {string} - */ -FinishedLoading.prototype.type = eventUtils.FINISHED_LOADING; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -FinishedLoading.prototype.toJson = function() { - const json = { - 'type': this.type, - }; - if (this.group) { - json['group'] = this.group; + fromJson(json) { + this.isBlank = false; + this.workspaceId = json['workspaceId']; + this.group = json['group']; } - if (this.workspaceId) { - json['workspaceId'] = this.workspaceId; - } - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -FinishedLoading.prototype.fromJson = function(json) { - this.isBlank = false; - this.workspaceId = json['workspaceId']; - this.group = json['group']; -}; +} registry.register( registry.Type.EVENT, eventUtils.FINISHED_LOADING, FinishedLoading); From cfe0d3e293acb94f78814befcbe7238afdc4913d Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 10 Feb 2022 17:03:56 -0800 Subject: [PATCH 5/9] refactor: convert events_abstract.js to an es6 class --- core/events/events_abstract.js | 159 +++++++++++++++++---------------- core/events/utils.js | 35 +++++--- core/mutator.js | 2 +- core/procedures.js | 14 ++- core/trashcan.js | 9 +- core/workspace_comment.js | 6 +- core/workspace_comment_svg.js | 7 +- 7 files changed, 131 insertions(+), 101 deletions(-) diff --git a/core/events/events_abstract.js b/core/events/events_abstract.js index 627c1fe9861..3582d57de35 100644 --- a/core/events/events_abstract.js +++ b/core/events/events_abstract.js @@ -24,98 +24,107 @@ const {Workspace} = goog.requireType('Blockly.Workspace'); /** * Abstract class for an event. - * @constructor - * @alias Blockly.Events.Abstract */ -const Abstract = function() { +class Abstract { /** - * Whether or not the event is blank (to be populated by fromJson). - * @type {?boolean} + * @alias Blockly.Events.Abstract */ - this.isBlank = null; + constructor() { + /** + * Whether or not the event is blank (to be populated by fromJson). + * @type {?boolean} + */ + this.isBlank = null; + + /** + * The workspace identifier for this event. + * @type {string|undefined} + */ + this.workspaceId = undefined; + + /** + * The event group id for the group this event belongs to. Groups define + * events that should be treated as an single action from the user's + * perspective, and should be undone together. + * @type {string} + */ + this.group = eventUtils.getGroup(); + + /** + * Sets whether the event should be added to the undo stack. + * @type {boolean} + */ + this.recordUndo = eventUtils.getRecordUndo(); + + /** + * Whether or not the event is a UI event. + * @type {boolean} + */ + this.isUiEvent = false; + + /** + * Type of this event. + * @type {string|undefined} + */ + this.type = undefined; + } /** - * The workspace identifier for this event. - * @type {string|undefined} + * Encode the event as JSON. + * @return {!Object} JSON representation. */ - this.workspaceId = undefined; + toJson() { + const json = {'type': this.type}; + if (this.group) { + json['group'] = this.group; + } + return json; + } /** - * The event group id for the group this event belongs to. Groups define - * events that should be treated as an single action from the user's - * perspective, and should be undone together. - * @type {string} + * Decode the JSON event. + * @param {!Object} json JSON representation. */ - this.group = eventUtils.getGroup(); + fromJson(json) { + this.isBlank = false; + this.group = json['group']; + } /** - * Sets whether the event should be added to the undo stack. - * @type {boolean} + * Does this event record any change of state? + * @return {boolean} True if null, false if something changed. */ - this.recordUndo = eventUtils.getRecordUndo(); + isNull() { + return false; + } /** - * Whether or not the event is a UI event. - * @type {boolean} + * Run an event. + * @param {boolean} _forward True if run forward, false if run backward (undo). */ - this.isUiEvent = false; -}; - -/** - * Encode the event as JSON. - * @return {!Object} JSON representation. - */ -Abstract.prototype.toJson = function() { - const json = {'type': this.type}; - if (this.group) { - json['group'] = this.group; + run(_forward) { + // Defined by subclasses. } - return json; -}; - -/** - * Decode the JSON event. - * @param {!Object} json JSON representation. - */ -Abstract.prototype.fromJson = function(json) { - this.isBlank = false; - this.group = json['group']; -}; -/** - * Does this event record any change of state? - * @return {boolean} True if null, false if something changed. - */ -Abstract.prototype.isNull = function() { - return false; -}; - -/** - * Run an event. - * @param {boolean} _forward True if run forward, false if run backward (undo). - */ -Abstract.prototype.run = function(_forward) { - // Defined by subclasses. -}; - -/** - * Get workspace the event belongs to. - * @return {!Workspace} The workspace the event belongs to. - * @throws {Error} if workspace is null. - * @protected - */ -Abstract.prototype.getEventWorkspace_ = function() { - let workspace; - if (this.workspaceId) { - const {Workspace} = goog.module.get('Blockly.Workspace'); - workspace = Workspace.getById(this.workspaceId); - } - if (!workspace) { - throw Error( - 'Workspace is null. Event must have been generated from real' + - ' Blockly events.'); + /** + * Get workspace the event belongs to. + * @return {!Workspace} The workspace the event belongs to. + * @throws {Error} if workspace is null. + * @protected + */ + getEventWorkspace_() { + let workspace; + if (this.workspaceId) { + const {Workspace} = goog.module.get('Blockly.Workspace'); + workspace = Workspace.getById(this.workspaceId); + } + if (!workspace) { + throw Error( + 'Workspace is null. Event must have been generated from real' + + ' Blockly events.'); + } + return workspace; } - return workspace; -}; +} exports.Abstract = Abstract; diff --git a/core/events/utils.js b/core/events/utils.js index 9f1c5c229e0..9a1be221fc0 100644 --- a/core/events/utils.js +++ b/core/events/utils.js @@ -22,6 +22,8 @@ const registry = goog.require('Blockly.registry'); /* eslint-disable-next-line no-unused-vars */ const {Abstract} = goog.requireType('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ +const {BlockChange} = goog.requireType('Blockly.Events.BlockChange'); +/* eslint-disable-next-line no-unused-vars */ const {BlockCreate} = goog.requireType('Blockly.Events.BlockCreate'); /* eslint-disable-next-line no-unused-vars */ const {BlockMove} = goog.requireType('Blockly.Events.BlockMove'); @@ -32,6 +34,8 @@ const {CommentCreate} = goog.requireType('Blockly.Events.CommentCreate'); /* eslint-disable-next-line no-unused-vars */ const {CommentMove} = goog.requireType('Blockly.Events.CommentMove'); /* eslint-disable-next-line no-unused-vars */ +const {ViewportChange} = goog.requireType('Blockly.Events.ViewportChange'); +/* eslint-disable-next-line no-unused-vars */ const {Workspace} = goog.requireType('Blockly.Workspace'); @@ -307,6 +311,7 @@ exports.BUMP_EVENTS = BUMP_EVENTS; /** * List of events queued for firing. + * @type {!Array} */ const FIRE_QUEUE = []; @@ -365,7 +370,9 @@ const filter = function(queueIn, forward) { if (!event.isNull()) { // Treat all UI events as the same type in hash table. const eventType = event.isUiEvent ? UI : event.type; - const key = [eventType, event.blockId, event.workspaceId].join(' '); + // TODO(#5927): Ceck whether `blockId` exists before accessing it. + const blockId = /** @type {BlockMove} */ (event).blockId; + const key = [eventType, blockId, event.workspaceId].join(' '); const lastEntry = hash[key]; const lastEvent = lastEntry ? lastEntry.event : null; @@ -376,22 +383,25 @@ const filter = function(queueIn, forward) { hash[key] = {event: event, index: i}; mergedQueue.push(event); } else if (event.type === MOVE && lastEntry.index === i - 1) { + const moveEvent = /** @type {!BlockMove} */ (event); // Merge move events. - lastEvent.newParentId = event.newParentId; - lastEvent.newInputName = event.newInputName; - lastEvent.newCoordinate = event.newCoordinate; + lastEvent.newParentId = moveEvent.newParentId; + lastEvent.newInputName = moveEvent.newInputName; + lastEvent.newCoordinate = moveEvent.newCoordinate; lastEntry.index = i; } else if ( event.type === CHANGE && event.element === lastEvent.element && event.name === lastEvent.name) { + const changeEvent = /** @type {!BlockChange} */ (event); // Merge change events. - lastEvent.newValue = event.newValue; + lastEvent.newValue = changeEvent.newValue; } else if (event.type === VIEWPORT_CHANGE) { + const viewportEvent = /** @type {!ViewportChange} */ (event); // Merge viewport change events. - lastEvent.viewTop = event.viewTop; - lastEvent.viewLeft = event.viewLeft; - lastEvent.scale = event.scale; - lastEvent.oldScale = event.oldScale; + lastEvent.viewTop = viewportEvent.viewTop; + lastEvent.viewLeft = viewportEvent.viewLeft; + lastEvent.scale = viewportEvent.scale; + lastEvent.oldScale = viewportEvent.oldScale; } else if (event.type === CLICK && lastEvent.type === BUBBLE_OPEN) { // Drop click events caused by opening/closing bubbles. } else { @@ -546,12 +556,13 @@ exports.get = get; */ const disableOrphans = function(event) { if (event.type === MOVE || event.type === CREATE) { - if (!event.workspaceId) { + const blockEvent = /** @type {!BlockMove|!BlockCreate} */ (event); + if (!blockEvent.workspaceId) { return; } const {Workspace} = goog.module.get('Blockly.Workspace'); - const eventWorkspace = Workspace.getById(event.workspaceId); - let block = eventWorkspace.getBlockById(event.blockId); + const eventWorkspace = Workspace.getById(blockEvent.workspaceId); + let block = eventWorkspace.getBlockById(blockEvent.blockId); if (block) { // Changing blocks as part of this event shouldn't be undoable. const initialUndoFlag = recordUndo; diff --git a/core/mutator.js b/core/mutator.js index d5079c63dc3..39964d7ced9 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -405,7 +405,7 @@ class Mutator extends Icon { */ workspaceChanged_(e) { if (!(e.isUiEvent || - (e.type === eventUtils.CHANGE && e.element === 'disabled') || + (e.type === eventUtils.CHANGE && /** @type {!BlockChange} */(e).element === 'disabled') || e.type === eventUtils.CREATE)) { this.updateWorkspace_(); } diff --git a/core/procedures.js b/core/procedures.js index ab6b9125c39..aa6e2d5a2a4 100644 --- a/core/procedures.js +++ b/core/procedures.js @@ -25,6 +25,8 @@ const {Blocks} = goog.require('Blockly.blocks'); /* eslint-disable-next-line no-unused-vars */ const {Block} = goog.requireType('Blockly.Block'); /* eslint-disable-next-line no-unused-vars */ +const {BubbleOpen} = goog.requireType('Blockly.Events.BubbleOpen'); +/* eslint-disable-next-line no-unused-vars */ const {Field} = goog.requireType('Blockly.Field'); const {Msg} = goog.require('Blockly.Msg'); const {Names} = goog.require('Blockly.Names'); @@ -325,12 +327,16 @@ const updateMutatorFlyout = function(workspace) { * @package */ const mutatorOpenListener = function(e) { - if (!(e.type === eventUtils.BUBBLE_OPEN && e.bubbleType === 'mutator' && - e.isOpen)) { + if (e.type === eventUtils.BUBBLE_OPEN) { return; } - const workspaceId = /** @type {string} */ (e.workspaceId); - const block = Workspace.getById(workspaceId).getBlockById(e.blockId); + const bubbleEvent = /** @type {!BubbleOpen} */ (e); + if (!(bubbleEvent.bubbleType === 'mutator' && bubbleEvent.isOpen)) { + return; + } + const workspaceId = /** @type {string} */ (bubbleEvent.workspaceId); + const block = + Workspace.getById(workspaceId).getBlockById(bubbleEvent.blockId); const type = block.type; if (type !== 'procedures_defnoreturn' && type !== 'procedures_defreturn') { return; diff --git a/core/trashcan.js b/core/trashcan.js index 3b7cdaa4cdc..459f97a330f 100644 --- a/core/trashcan.js +++ b/core/trashcan.js @@ -26,6 +26,8 @@ const uiPosition = goog.require('Blockly.uiPosition'); /* eslint-disable-next-line no-unused-vars */ const {Abstract} = goog.requireType('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ +const {BlockDelete} = goog.requireType('Blockly.Events.BlockDelete'); +/* eslint-disable-next-line no-unused-vars */ const {BlocklyOptions} = goog.requireType('Blockly.BlocklyOptions'); const {ComponentManager} = goog.require('Blockly.ComponentManager'); const {DeleteArea} = goog.require('Blockly.DeleteArea'); @@ -607,11 +609,12 @@ class Trashcan extends DeleteArea { * @private */ onDelete_(event) { - if (this.workspace_.options.maxTrashcanContents <= 0) { + if (this.workspace_.options.maxTrashcanContents <= 0 || event.type !== eventUtils.BLOCK_DELETE) { return; } - if (event.type === eventUtils.BLOCK_DELETE && !event.wasShadow) { - const cleanedJson = this.cleanBlockJson_(event.oldJson); + const deleteEvent = /** @type {!BlockDelete} */ (event); + if (event.type === eventUtils.BLOCK_DELETE && !deleteEvent.wasShadow) { + const cleanedJson = this.cleanBlockJson_(deleteEvent.oldJson); if (this.contents_.indexOf(cleanedJson) !== -1) { return; } diff --git a/core/workspace_comment.js b/core/workspace_comment.js index 2e16c80bd15..e17ed4a354e 100644 --- a/core/workspace_comment.js +++ b/core/workspace_comment.js @@ -19,6 +19,8 @@ const eventUtils = goog.require('Blockly.Events.utils'); const idGenerator = goog.require('Blockly.utils.idGenerator'); const xml = goog.require('Blockly.utils.xml'); const {Coordinate} = goog.require('Blockly.utils.Coordinate'); +/** @suppress {extraRequire} */ +const {CommentMove} = goog.require('Blockly.Events.CommentMove'); /* eslint-disable-next-line no-unused-vars */ const {Workspace} = goog.requireType('Blockly.Workspace'); /** @suppress {extraRequire} */ @@ -27,8 +29,6 @@ goog.require('Blockly.Events.CommentChange'); goog.require('Blockly.Events.CommentCreate'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.CommentDelete'); -/** @suppress {extraRequire} */ -goog.require('Blockly.Events.CommentMove'); /** @@ -201,7 +201,7 @@ class WorkspaceComment { * @package */ moveBy(dx, dy) { - const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(this); + const event = /** @type {!CommentMove} */(new (eventUtils.get(eventUtils.COMMENT_MOVE))(this)); this.xy_.translate(dx, dy); event.recordNew(); eventUtils.fire(event); diff --git a/core/workspace_comment_svg.js b/core/workspace_comment_svg.js index 9da7d8dd6e8..1194100ca90 100644 --- a/core/workspace_comment_svg.js +++ b/core/workspace_comment_svg.js @@ -25,6 +25,9 @@ const eventUtils = goog.require('Blockly.Events.utils'); const svgMath = goog.require('Blockly.utils.svgMath'); /* eslint-disable-next-line no-unused-vars */ const {BlockDragSurfaceSvg} = goog.requireType('Blockly.BlockDragSurfaceSvg'); +/* eslint-disable-next-line no-unused-vars */ +/** @suppress {extraRequire} */ +const {CommentMove} = goog.require('Blockly.Events.CommentMove'); const {Coordinate} = goog.require('Blockly.utils.Coordinate'); /* eslint-disable-next-line no-unused-vars */ const {IBoundedElement} = goog.require('Blockly.IBoundedElement'); @@ -42,8 +45,6 @@ goog.require('Blockly.Events.CommentCreate'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.CommentDelete'); /** @suppress {extraRequire} */ -goog.require('Blockly.Events.CommentMove'); -/** @suppress {extraRequire} */ goog.require('Blockly.Events.Selected'); @@ -427,7 +428,7 @@ class WorkspaceCommentSvg extends WorkspaceComment { * @package */ moveBy(dx, dy) { - const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))(this); + const event = /** @type {!CommentMove} */(new (eventUtils.get(eventUtils.COMMENT_MOVE))(this)); // TODO: Do I need to look up the relative to surface XY position here? const xy = this.getRelativeToSurfaceXY(); this.translate(xy.x + dx, xy.y + dy); From 19b254bc7b15b5ca2404c8a224e5564713ce972c Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 10 Feb 2022 17:04:42 -0800 Subject: [PATCH 6/9] chore: format --- core/events/events_abstract.js | 3 ++- core/generator.js | 31 +++++++++++++++++-------------- core/mutator.js | 3 ++- core/options.js | 18 ++++++++++-------- core/trashcan.js | 3 ++- core/workspace_comment.js | 3 ++- core/workspace_comment_svg.js | 3 ++- 7 files changed, 37 insertions(+), 27 deletions(-) diff --git a/core/events/events_abstract.js b/core/events/events_abstract.js index 3582d57de35..63635cbbac8 100644 --- a/core/events/events_abstract.js +++ b/core/events/events_abstract.js @@ -100,7 +100,8 @@ class Abstract { /** * Run an event. - * @param {boolean} _forward True if run forward, false if run backward (undo). + * @param {boolean} _forward True if run forward, false if run backward + * (undo). */ run(_forward) { // Defined by subclasses. diff --git a/core/generator.js b/core/generator.js index f3df65a1585..27614f757bd 100644 --- a/core/generator.js +++ b/core/generator.js @@ -143,7 +143,8 @@ class Generator { workspaceToCode(workspace) { if (!workspace) { // Backwards compatibility from before there could be multiple workspaces. - console.warn('No workspace specified in workspaceToCode call. Guessing.'); + console.warn( + 'No workspace specified in workspaceToCode call. Guessing.'); workspace = common.getMainWorkspace(); } let code = []; @@ -220,7 +221,8 @@ class Generator { * Generate code for the specified block (and attached blocks). * The generator must be initialized before calling this function. * @param {Block} block The block to generate code for. - * @param {boolean=} opt_thisOnly True to generate code for only this statement. + * @param {boolean=} opt_thisOnly True to generate code for only this + * statement. * @return {string|!Array} For statement blocks, the generated code. * For value blocks, an array containing the generated code and an * operator order value. Returns '' if block is null. @@ -251,7 +253,8 @@ class Generator { // First argument to func.call is the value of 'this' in the generator. // Prior to 24 September 2013 'this' was the only way to access the block. // The current preferred method of accessing the block is through the second - // argument to func.call, which becomes the first parameter to the generator. + // argument to func.call, which becomes the first parameter to the + // generator. let code = func.call(block, block); if (Array.isArray(code)) { // Value blocks return tuples of code and operator order. @@ -278,8 +281,8 @@ class Generator { * Generate code representing the specified value input. * @param {!Block} block The block containing the input. * @param {string} name The name of the input. - * @param {number} outerOrder The maximum binding strength (minimum order value) - * of any operators adjacent to "block". + * @param {number} outerOrder The maximum binding strength (minimum order + * value) of any operators adjacent to "block". * @return {string} Generated code or '' if no blocks are connected or the * specified input does not exist. */ @@ -421,10 +424,10 @@ class Generator { /** * Define a developer-defined function (not a user-defined procedure) to be - * included in the generated code. Used for creating private helper functions. - * The first time this is called with a given desiredName, the code is - * saved and an actual name is generated. Subsequent calls with the - * same desiredName have no effect but have the same return value. + * included in the generated code. Used for creating private helper + * functions. The first time this is called with a given desiredName, the code + * is saved and an actual name is generated. Subsequent calls with the same + * desiredName have no effect but have the same return value. * * It is up to the caller to make sure the same desiredName is not * used for different helper functions (e.g. use "colourRandom" and @@ -485,9 +488,9 @@ class Generator { /** * Common tasks for generating code from blocks. This is called from * blockToCode and is called on every block, not just top level blocks. - * Subclasses may override this, e.g. to generate code for statements following - * the block, or to handle comments for the specified block and any connected - * value blocks. + * Subclasses may override this, e.g. to generate code for statements + * following the block, or to handle comments for the specified block and any + * connected value blocks. * @param {!Block} _block The current block. * @param {string} code The code created for this block. * @param {boolean=} _opt_thisOnly True to generate code for only this @@ -502,8 +505,8 @@ class Generator { /** * Hook for code to run at end of code generation. - * Subclasses may override this, e.g. to prepend the generated code with import - * statements or variable definitions. + * Subclasses may override this, e.g. to prepend the generated code with + * import statements or variable definitions. * @param {string} code Generated code. * @return {string} Completed code. */ diff --git a/core/mutator.js b/core/mutator.js index 39964d7ced9..0794e32ca64 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -405,7 +405,8 @@ class Mutator extends Icon { */ workspaceChanged_(e) { if (!(e.isUiEvent || - (e.type === eventUtils.CHANGE && /** @type {!BlockChange} */(e).element === 'disabled') || + (e.type === eventUtils.CHANGE && + /** @type {!BlockChange} */ (e).element === 'disabled') || e.type === eventUtils.CREATE)) { this.updateWorkspace_(); } diff --git a/core/options.js b/core/options.js index 78ebeaf24ff..f4c315fa013 100644 --- a/core/options.js +++ b/core/options.js @@ -192,10 +192,10 @@ class Options { /** * If set, sets the translation of the workspace to match the scrollbars. - * @type {undefined|function(!{x:number,y:number}):void} A function that sets - * the translation of the workspace to match the scrollbars. The argument - * Contains an x and/or y property which is a float between 0 and 1 - * specifying the degree of scrolling. + * @type {undefined|function(!{x:number,y:number}):void} A function that + * sets the translation of the workspace to match the scrollbars. The + * argument Contains an x and/or y property which is a float between 0 + * and 1 specifying the degree of scrolling. */ this.setMetrics = undefined; @@ -217,7 +217,8 @@ class Options { static parseMoveOptions_(options, hasCategories) { const move = options['move'] || {}; const moveOptions = {}; - if (move['scrollbars'] === undefined && options['scrollbars'] === undefined) { + if (move['scrollbars'] === undefined && + options['scrollbars'] === undefined) { moveOptions.scrollbars = hasCategories; } else if (typeof move['scrollbars'] === 'object') { moveOptions.scrollbars = {}; @@ -226,7 +227,8 @@ class Options { // Convert scrollbars object to boolean if they have the same value. // This allows us to easily check for whether any scrollbars exist using // !!moveOptions.scrollbars. - if (moveOptions.scrollbars.horizontal && moveOptions.scrollbars.vertical) { + if (moveOptions.scrollbars.horizontal && + moveOptions.scrollbars.vertical) { moveOptions.scrollbars = true; } else if ( !moveOptions.scrollbars.horizontal && @@ -323,8 +325,8 @@ class Options { } /** - * Parse the user-specified theme options, using the classic theme as a default. - * https://developers.google.com/blockly/guides/configure/web/themes + * Parse the user-specified theme options, using the classic theme as a + * default. https://developers.google.com/blockly/guides/configure/web/themes * @param {!Object} options Dictionary of options. * @return {!Theme} A Blockly Theme. * @private diff --git a/core/trashcan.js b/core/trashcan.js index 459f97a330f..b9ffce3adaa 100644 --- a/core/trashcan.js +++ b/core/trashcan.js @@ -609,7 +609,8 @@ class Trashcan extends DeleteArea { * @private */ onDelete_(event) { - if (this.workspace_.options.maxTrashcanContents <= 0 || event.type !== eventUtils.BLOCK_DELETE) { + if (this.workspace_.options.maxTrashcanContents <= 0 || + event.type !== eventUtils.BLOCK_DELETE) { return; } const deleteEvent = /** @type {!BlockDelete} */ (event); diff --git a/core/workspace_comment.js b/core/workspace_comment.js index e17ed4a354e..9a018d1adfe 100644 --- a/core/workspace_comment.js +++ b/core/workspace_comment.js @@ -201,7 +201,8 @@ class WorkspaceComment { * @package */ moveBy(dx, dy) { - const event = /** @type {!CommentMove} */(new (eventUtils.get(eventUtils.COMMENT_MOVE))(this)); + const event = /** @type {!CommentMove} */ ( + new (eventUtils.get(eventUtils.COMMENT_MOVE))(this)); this.xy_.translate(dx, dy); event.recordNew(); eventUtils.fire(event); diff --git a/core/workspace_comment_svg.js b/core/workspace_comment_svg.js index 1194100ca90..ad6fbc3f5a5 100644 --- a/core/workspace_comment_svg.js +++ b/core/workspace_comment_svg.js @@ -428,7 +428,8 @@ class WorkspaceCommentSvg extends WorkspaceComment { * @package */ moveBy(dx, dy) { - const event = /** @type {!CommentMove} */(new (eventUtils.get(eventUtils.COMMENT_MOVE))(this)); + const event = /** @type {!CommentMove} */ ( + new (eventUtils.get(eventUtils.COMMENT_MOVE))(this)); // TODO: Do I need to look up the relative to surface XY position here? const xy = this.getRelativeToSurfaceXY(); this.translate(xy.x + dx, xy.y + dy); From de7af56c26558708e9524ac85fa915426a7ae61e Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 10 Feb 2022 17:07:32 -0800 Subject: [PATCH 7/9] chore: fix lint --- core/generator.js | 4 ++-- core/workspace_comment.js | 2 +- core/workspace_comment_svg.js | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/generator.js b/core/generator.js index 27614f757bd..b437b5f1599 100644 --- a/core/generator.js +++ b/core/generator.js @@ -530,7 +530,7 @@ class Generator { // Optionally override return line; } -}; +} Object.defineProperties(Generator.prototype, { /** @@ -559,6 +559,6 @@ Object.defineProperties(Generator.prototype, { this.nameDB_ = nameDb; }, }, -}) +}); exports.Generator = Generator; diff --git a/core/workspace_comment.js b/core/workspace_comment.js index 9a018d1adfe..19299a7ab0c 100644 --- a/core/workspace_comment.js +++ b/core/workspace_comment.js @@ -19,7 +19,7 @@ const eventUtils = goog.require('Blockly.Events.utils'); const idGenerator = goog.require('Blockly.utils.idGenerator'); const xml = goog.require('Blockly.utils.xml'); const {Coordinate} = goog.require('Blockly.utils.Coordinate'); -/** @suppress {extraRequire} */ +/* eslint-disable-next-line no-unused-vars */ const {CommentMove} = goog.require('Blockly.Events.CommentMove'); /* eslint-disable-next-line no-unused-vars */ const {Workspace} = goog.requireType('Blockly.Workspace'); diff --git a/core/workspace_comment_svg.js b/core/workspace_comment_svg.js index ad6fbc3f5a5..0288370c859 100644 --- a/core/workspace_comment_svg.js +++ b/core/workspace_comment_svg.js @@ -26,7 +26,6 @@ const svgMath = goog.require('Blockly.utils.svgMath'); /* eslint-disable-next-line no-unused-vars */ const {BlockDragSurfaceSvg} = goog.requireType('Blockly.BlockDragSurfaceSvg'); /* eslint-disable-next-line no-unused-vars */ -/** @suppress {extraRequire} */ const {CommentMove} = goog.require('Blockly.Events.CommentMove'); const {Coordinate} = goog.require('Blockly.utils.Coordinate'); /* eslint-disable-next-line no-unused-vars */ From 46f7ffd42f7169aa0640144be6d3e5c71db1bd40 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 10 Feb 2022 17:08:59 -0800 Subject: [PATCH 8/9] chore: rebuild --- tests/deps.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/deps.js b/tests/deps.js index df0e52f0820..6118ba8b1b3 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -66,7 +66,7 @@ goog.addDependency('../../core/events/events_var_delete.js', ['Blockly.Events.Va goog.addDependency('../../core/events/events_var_rename.js', ['Blockly.Events.VarRename'], ['Blockly.Events.VarBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/events_viewport.js', ['Blockly.Events.ViewportChange'], ['Blockly.Events.UiBase', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/utils.js', ['Blockly.Events.utils'], ['Blockly.registry', 'Blockly.utils.idGenerator'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/events/workspace_events.js', ['Blockly.Events.FinishedLoading'], ['Blockly.Events.Abstract', 'Blockly.Events.utils', 'Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/events/workspace_events.js', ['Blockly.Events.FinishedLoading'], ['Blockly.Events.Abstract', 'Blockly.Events.utils', 'Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/extensions.js', ['Blockly.Extensions'], ['Blockly.utils.parsing'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/field.js', ['Blockly.Field'], ['Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Events.utils', 'Blockly.Gesture', 'Blockly.IASTNodeLocationSvg', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.IKeyboardAccessible', 'Blockly.IRegistrable', 'Blockly.MarkerManager', 'Blockly.Tooltip', 'Blockly.WidgetDiv', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.parsing', 'Blockly.utils.style', 'Blockly.utils.userAgent', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/field_angle.js', ['Blockly.FieldAngle'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.FieldTextInput', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.object', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'}); From f7950c05b0b856e91f27d78df18d924efbc1373c Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Wed, 16 Feb 2022 10:27:19 -0800 Subject: [PATCH 9/9] chore: respond to PR feedback --- core/events/events.js | 4 ++-- core/events/events_abstract.js | 1 + core/events/events_block_base.js | 6 +++--- core/events/events_comment_base.js | 6 +++--- core/events/events_ui_base.js | 6 +++--- core/events/events_var_base.js | 6 +++--- core/events/utils.js | 2 +- core/events/workspace_events.js | 6 +++--- core/generator.js | 2 +- core/options.js | 2 +- core/procedures.js | 2 +- 11 files changed, 22 insertions(+), 21 deletions(-) diff --git a/core/events/events.js b/core/events/events.js index e6e1ef5b493..cd393c30946 100644 --- a/core/events/events.js +++ b/core/events/events.js @@ -17,7 +17,7 @@ goog.module('Blockly.Events'); const deprecation = goog.require('Blockly.utils.deprecation'); const eventUtils = goog.require('Blockly.Events.utils'); -const {Abstract} = goog.require('Blockly.Events.Abstract'); +const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract'); const {BlockBase} = goog.require('Blockly.Events.BlockBase'); const {BlockChange} = goog.require('Blockly.Events.BlockChange'); const {BlockCreate} = goog.require('Blockly.Events.BlockCreate'); @@ -47,7 +47,7 @@ const {ViewportChange} = goog.require('Blockly.Events.ViewportChange'); // Events. -exports.Abstract = Abstract; +exports.Abstract = AbstractEvent; exports.BubbleOpen = BubbleOpen; exports.BlockBase = BlockBase; exports.BlockChange = BlockChange; diff --git a/core/events/events_abstract.js b/core/events/events_abstract.js index 63635cbbac8..351e58e00a1 100644 --- a/core/events/events_abstract.js +++ b/core/events/events_abstract.js @@ -24,6 +24,7 @@ const {Workspace} = goog.requireType('Blockly.Workspace'); /** * Abstract class for an event. + * @abstract */ class Abstract { /** diff --git a/core/events/events_block_base.js b/core/events/events_block_base.js index 935575b1ad9..48ac6ebf5de 100644 --- a/core/events/events_block_base.js +++ b/core/events/events_block_base.js @@ -15,16 +15,16 @@ */ goog.module('Blockly.Events.BlockBase'); -const {Abstract} = goog.require('Blockly.Events.Abstract'); +const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ const {Block} = goog.requireType('Blockly.Block'); /** * Abstract class for a block event. - * @extends {Abstract} + * @extends {AbstractEvent} */ -class BlockBase extends Abstract { +class BlockBase extends AbstractEvent { /** * @param {!Block=} opt_block The block this event corresponds to. * Undefined for a blank event. diff --git a/core/events/events_comment_base.js b/core/events/events_comment_base.js index c77e6e7613b..bba5cfa53b1 100644 --- a/core/events/events_comment_base.js +++ b/core/events/events_comment_base.js @@ -18,7 +18,7 @@ goog.module('Blockly.Events.CommentBase'); const Xml = goog.require('Blockly.Xml'); const eventUtils = goog.require('Blockly.Events.utils'); const utilsXml = goog.require('Blockly.utils.xml'); -const {Abstract} = goog.require('Blockly.Events.Abstract'); +const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ const {CommentCreate} = goog.requireType('Blockly.Events.CommentCreate'); /* eslint-disable-next-line no-unused-vars */ @@ -29,9 +29,9 @@ const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment'); /** * Abstract class for a comment event. - * @extends {Abstract} + * @extends {AbstractEvent} */ -class CommentBase extends Abstract { +class CommentBase extends AbstractEvent { /** * @param {!WorkspaceComment=} opt_comment The comment this event * corresponds to. Undefined for a blank event. diff --git a/core/events/events_ui_base.js b/core/events/events_ui_base.js index eb2bf4fcf53..81fed30be1c 100644 --- a/core/events/events_ui_base.js +++ b/core/events/events_ui_base.js @@ -17,7 +17,7 @@ */ goog.module('Blockly.Events.UiBase'); -const {Abstract} = goog.require('Blockly.Events.Abstract'); +const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract'); /** @@ -26,9 +26,9 @@ const {Abstract} = goog.require('Blockly.Events.Abstract'); * editing to work (e.g. scrolling the workspace, zooming, opening toolbox * categories). * UI events do not undo or redo. - * @extends {Abstract} + * @extends {AbstractEvent} */ -class UiBase extends Abstract { +class UiBase extends AbstractEvent { /** * @param {string=} opt_workspaceId The workspace identifier for this event. * Undefined for a blank event. diff --git a/core/events/events_var_base.js b/core/events/events_var_base.js index 82eef232344..41aa9173caa 100644 --- a/core/events/events_var_base.js +++ b/core/events/events_var_base.js @@ -15,16 +15,16 @@ */ goog.module('Blockly.Events.VarBase'); -const {Abstract} = goog.require('Blockly.Events.Abstract'); +const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ const {VariableModel} = goog.requireType('Blockly.VariableModel'); /** * Abstract class for a variable event. - * @extends {Abstract} + * @extends {AbstractEvent} */ -class VarBase extends Abstract { +class VarBase extends AbstractEvent { /** * @param {!VariableModel=} opt_variable The variable this event * corresponds to. Undefined for a blank event. diff --git a/core/events/utils.js b/core/events/utils.js index 9a1be221fc0..cf76736234a 100644 --- a/core/events/utils.js +++ b/core/events/utils.js @@ -371,7 +371,7 @@ const filter = function(queueIn, forward) { // Treat all UI events as the same type in hash table. const eventType = event.isUiEvent ? UI : event.type; // TODO(#5927): Ceck whether `blockId` exists before accessing it. - const blockId = /** @type {BlockMove} */ (event).blockId; + const blockId = /** @type {*} */ (event).blockId; const key = [eventType, blockId, event.workspaceId].join(' '); const lastEntry = hash[key]; diff --git a/core/events/workspace_events.js b/core/events/workspace_events.js index 7d34de0871f..77abc95797e 100644 --- a/core/events/workspace_events.js +++ b/core/events/workspace_events.js @@ -17,7 +17,7 @@ goog.module('Blockly.Events.FinishedLoading'); const eventUtils = goog.require('Blockly.Events.utils'); const registry = goog.require('Blockly.registry'); -const {Abstract} = goog.require('Blockly.Events.Abstract'); +const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract'); /* eslint-disable-next-line no-unused-vars */ const {Workspace} = goog.requireType('Blockly.Workspace'); @@ -27,9 +27,9 @@ const {Workspace} = goog.requireType('Blockly.Workspace'); * Used to notify the developer when the workspace has finished loading (i.e * domToWorkspace). * Finished loading events do not record undo or redo. - * @extends {Abstract} + * @extends {AbstractEvent} */ -class FinishedLoading extends Abstract { +class FinishedLoading extends AbstractEvent { /** * @param {!Workspace=} opt_workspace The workspace that has finished * loading. Undefined for a blank event. diff --git a/core/generator.js b/core/generator.js index b437b5f1599..811218aa3ea 100644 --- a/core/generator.js +++ b/core/generator.js @@ -220,7 +220,7 @@ class Generator { /** * Generate code for the specified block (and attached blocks). * The generator must be initialized before calling this function. - * @param {Block} block The block to generate code for. + * @param {?Block} block The block to generate code for. * @param {boolean=} opt_thisOnly True to generate code for only this * statement. * @return {string|!Array} For statement blocks, the generated code. diff --git a/core/options.js b/core/options.js index f4c315fa013..5d7b032984b 100644 --- a/core/options.js +++ b/core/options.js @@ -180,7 +180,7 @@ class Options { * The parent of the current workspace, or null if there is no parent * workspace. We can assert that this is of type WorkspaceSvg as opposed to * Workspace as this is only used in a rendered workspace. - * @type {WorkspaceSvg} + * @type {?WorkspaceSvg} */ this.parentWorkspace = options['parentWorkspace']; diff --git a/core/procedures.js b/core/procedures.js index aa6e2d5a2a4..308b1fff740 100644 --- a/core/procedures.js +++ b/core/procedures.js @@ -327,7 +327,7 @@ const updateMutatorFlyout = function(workspace) { * @package */ const mutatorOpenListener = function(e) { - if (e.type === eventUtils.BUBBLE_OPEN) { + if (e.type !== eventUtils.BUBBLE_OPEN) { return; } const bubbleEvent = /** @type {!BubbleOpen} */ (e);