diff --git a/blocks/logic.ts b/blocks/logic.ts index d2a7405fffa..6cbcd83a1f9 100644 --- a/blocks/logic.ts +++ b/blocks/logic.ts @@ -64,6 +64,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'name': 'DO0', }, ], + 'output': null, 'previousStatement': null, 'nextStatement': null, 'style': 'logic_blocks', @@ -97,6 +98,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'name': 'ELSE', }, ], + 'output': null, 'previousStatement': null, 'nextStatement': null, 'style': 'logic_blocks', @@ -228,6 +230,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'type': 'controls_if_if', 'message0': '%{BKY_CONTROLS_IF_IF_TITLE_IF}', 'nextStatement': null, + 'output': null, 'enableContextMenu': false, 'style': 'logic_blocks', 'tooltip': '%{BKY_CONTROLS_IF_IF_TOOLTIP}', @@ -238,6 +241,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'message0': '%{BKY_CONTROLS_IF_ELSEIF_TITLE_ELSEIF}', 'previousStatement': null, 'nextStatement': null, + 'output': null, 'enableContextMenu': false, 'style': 'logic_blocks', 'tooltip': '%{BKY_CONTROLS_IF_ELSEIF_TOOLTIP}', @@ -247,6 +251,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'type': 'controls_if_else', 'message0': '%{BKY_CONTROLS_IF_ELSE_TITLE_ELSE}', 'previousStatement': null, + 'output': null, 'enableContextMenu': false, 'style': 'logic_blocks', 'tooltip': '%{BKY_CONTROLS_IF_ELSE_TOOLTIP}', diff --git a/blocks/loops.ts b/blocks/loops.ts index 6d450e53215..a79b2ff8a45 100644 --- a/blocks/loops.ts +++ b/blocks/loops.ts @@ -53,6 +53,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ ], 'previousStatement': null, 'nextStatement': null, + 'output': null, 'style': 'loop_blocks', 'tooltip': '%{BKY_CONTROLS_REPEAT_TOOLTIP}', 'helpUrl': '%{BKY_CONTROLS_REPEAT_HELPURL}', @@ -80,6 +81,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ ], 'previousStatement': null, 'nextStatement': null, + 'output': null, 'style': 'loop_blocks', 'tooltip': '%{BKY_CONTROLS_REPEAT_TOOLTIP}', 'helpUrl': '%{BKY_CONTROLS_REPEAT_HELPURL}', @@ -112,6 +114,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ ], 'previousStatement': null, 'nextStatement': null, + 'output': null, 'style': 'loop_blocks', 'helpUrl': '%{BKY_CONTROLS_WHILEUNTIL_HELPURL}', 'extensions': ['controls_whileUntil_tooltip'], @@ -155,6 +158,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'inputsInline': true, 'previousStatement': null, 'nextStatement': null, + 'output': null, 'style': 'loop_blocks', 'helpUrl': '%{BKY_CONTROLS_FOR_HELPURL}', 'extensions': ['contextMenu_newGetVariableBlock', 'controls_for_tooltip'], @@ -184,6 +188,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ ], 'previousStatement': null, 'nextStatement': null, + 'output': null, 'style': 'loop_blocks', 'helpUrl': '%{BKY_CONTROLS_FOREACH_HELPURL}', 'extensions': [ @@ -206,6 +211,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ }, ], 'previousStatement': null, + 'output': null, 'style': 'loop_blocks', 'helpUrl': '%{BKY_CONTROLS_FLOW_STATEMENTS_HELPURL}', 'suppressPrefixSuffix': true, diff --git a/blocks/math.ts b/blocks/math.ts index e5aef5fbb6e..238148e0689 100644 --- a/blocks/math.ts +++ b/blocks/math.ts @@ -208,6 +208,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ ], 'previousStatement': null, 'nextStatement': null, + 'output': null, 'style': 'variable_blocks', 'helpUrl': '%{BKY_MATH_CHANGE_HELPURL}', 'extensions': ['math_change_tooltip'], diff --git a/blocks/text.ts b/blocks/text.ts index a7ad5374ac4..458a9bab7b2 100644 --- a/blocks/text.ts +++ b/blocks/text.ts @@ -68,6 +68,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'name': 'STACK', }, ], + 'output': null, 'style': 'text_blocks', 'tooltip': '%{BKY_TEXT_CREATE_JOIN_TOOLTIP}', 'enableContextMenu': false, @@ -78,6 +79,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'previousStatement': null, 'nextStatement': null, 'style': 'text_blocks', + 'output': null, 'tooltip': '%{BKY_TEXT_CREATE_JOIN_ITEM_TOOLTIP}', 'enableContextMenu': false, }, @@ -97,6 +99,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ ], 'previousStatement': null, 'nextStatement': null, + 'output': null, 'style': 'text_blocks', 'extensions': ['text_append_tooltip'], }, @@ -387,6 +390,7 @@ blocks['text_print'] = { 'name': 'TEXT', }, ], + 'output': null, 'previousStatement': null, 'nextStatement': null, 'style': 'text_blocks', diff --git a/blocks/variables.ts b/blocks/variables.ts index 4f1f640fa81..e2bf96574f3 100644 --- a/blocks/variables.ts +++ b/blocks/variables.ts @@ -60,6 +60,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ ], 'previousStatement': null, 'nextStatement': null, + 'output': null, 'style': 'variable_blocks', 'tooltip': '%{BKY_VARIABLES_SET_TOOLTIP}', 'helpUrl': '%{BKY_VARIABLES_SET_HELPURL}', diff --git a/blocks/variables_dynamic.ts b/blocks/variables_dynamic.ts index 8afd24cf2e3..d2944115d05 100644 --- a/blocks/variables_dynamic.ts +++ b/blocks/variables_dynamic.ts @@ -59,6 +59,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([ 'name': 'VALUE', }, ], + 'output': null, 'previousStatement': null, 'nextStatement': null, 'style': 'variable_dynamic_blocks', diff --git a/core/block.ts b/core/block.ts index af44facda5d..7070129705a 100644 --- a/core/block.ts +++ b/core/block.ts @@ -54,6 +54,7 @@ import * as idGenerator from './utils/idgenerator.js'; import * as parsing from './utils/parsing.js'; import {Size} from './utils/size.js'; import type {Workspace} from './workspace.js'; +import { BlockArg, JsonBlockDefinition } from './interfaces/i_json_block_definition.js' /** * Class for one block. @@ -1709,11 +1710,11 @@ export class Block { * * @param json Structured data describing the block. */ - jsonInit(json: AnyDuringMigration) { - const warningPrefix = json['type'] ? 'Block "' + json['type'] + '": ' : ''; + jsonInit(json: JsonBlockDefinition) { + const warningPrefix = json.type ? 'Block "' + json.type + '": ' : ''; // Validate inputs. - if (json['output'] && json['previousStatement']) { + if (json.output && json.previousStatement) { throw Error( warningPrefix + 'Must not have both an output and a previousStatement.', ); @@ -1721,8 +1722,8 @@ export class Block { // Validate that each arg has a corresponding message let n = 0; - while (json['args' + n]) { - if (json['message' + n] === undefined) { + while (json[`args${n}`]) { + if (json[`message${n}`] === undefined) { throw Error( warningPrefix + `args${n} must have a corresponding message (message${n}).`, @@ -1732,17 +1733,9 @@ export class Block { } // Set basic properties of block. - // Makes styles backward compatible with old way of defining hat style. - if (json['style'] && json['style'].hat) { - this.hat = json['style'].hat; - // Must set to null so it doesn't error when checking for style and - // colour. - json['style'] = null; - } - - if (json['style'] && json['colour']) { + if (json.style && json.colour) { throw Error(warningPrefix + 'Must not have both a colour and a style.'); - } else if (json['style']) { + } else if (json.style) { this.jsonInitStyle(json, warningPrefix); } else { this.jsonInitColour(json, warningPrefix); @@ -1750,69 +1743,68 @@ export class Block { // Interpolate the message blocks. let i = 0; - while (json['message' + i] !== undefined) { + while (json[`message${i}`] !== undefined) { this.interpolate( - json['message' + i], - json['args' + i] || [], - // Backwards compatibility: lastDummyAlign aliases implicitAlign. - json['implicitAlign' + i] || json['lastDummyAlign' + i], + json[`message${i}`]!, + json[`args${i}`] || [], + json[`implicitAlign${i}`], warningPrefix, ); i++; } - if (json['inputsInline'] !== undefined) { + if (json.inputsInline !== undefined) { eventUtils.disable(); - this.setInputsInline(json['inputsInline']); + this.setInputsInline(json.inputsInline); eventUtils.enable(); } // Set output and previous/next connections. - if (json['output'] !== undefined) { - this.setOutput(true, json['output']); + if (json.output !== undefined) { + this.setOutput(true, json.output); } - if (json['outputShape'] !== undefined) { - this.setOutputShape(json['outputShape']); + if (json.outputShape !== undefined) { + this.setOutputShape(json.outputShape); } - if (json['previousStatement'] !== undefined) { - this.setPreviousStatement(true, json['previousStatement']); + if (json.previousStatement !== undefined) { + this.setPreviousStatement(true, json.previousStatement); } - if (json['nextStatement'] !== undefined) { - this.setNextStatement(true, json['nextStatement']); + if (json.nextStatement !== undefined) { + this.setNextStatement(true, json.nextStatement); } - if (json['tooltip'] !== undefined) { - const rawValue = json['tooltip']; + if (json.tooltip !== undefined) { + const rawValue = json.tooltip; const localizedText = parsing.replaceMessageReferences(rawValue); this.setTooltip(localizedText); } - if (json['enableContextMenu'] !== undefined) { - this.contextMenu = !!json['enableContextMenu']; + if (json.enableContextMenu !== undefined) { + this.contextMenu = !!json.enableContextMenu; } - if (json['suppressPrefixSuffix'] !== undefined) { - this.suppressPrefixSuffix = !!json['suppressPrefixSuffix']; + if (json.suppressPrefixSuffix !== undefined) { + this.suppressPrefixSuffix = !!json.suppressPrefixSuffix; } - if (json['helpUrl'] !== undefined) { - const rawValue = json['helpUrl']; + if (json.helpUrl !== undefined) { + const rawValue = json.helpUrl; const localizedValue = parsing.replaceMessageReferences(rawValue); this.setHelpUrl(localizedValue); } - if (typeof json['extensions'] === 'string') { + if (typeof json.extensions === 'string') { console.warn( warningPrefix + "JSON attribute 'extensions' should be an array of" + " strings. Found raw string in JSON for '" + - json['type'] + + json.type + "' block.", ); - json['extensions'] = [json['extensions']]; // Correct and continue. + json.extensions = [json.extensions]; // Correct and continue. } // Add the mutator to the block. - if (json['mutator'] !== undefined) { - Extensions.apply(json['mutator'], this, true); + if (json.mutator !== undefined) { + Extensions.apply(json.mutator, this, true); } - const extensionNames = json['extensions']; + const extensionNames = json.extensions; if (Array.isArray(extensionNames)) { for (let j = 0; j < extensionNames.length; j++) { Extensions.apply(extensionNames[j], this, false); @@ -1826,12 +1818,12 @@ export class Block { * @param json Structured data describing the block. * @param warningPrefix Warning prefix string identifying block. */ - private jsonInitColour(json: AnyDuringMigration, warningPrefix: string) { - if ('colour' in json) { - if (json['colour'] === undefined) { + private jsonInitColour(json: JsonBlockDefinition, warningPrefix: string) { + if (json.colour) { + if (json.colour === undefined) { console.warn(warningPrefix + 'Undefined colour value.'); } else { - const rawValue = json['colour']; + const rawValue = json.colour; try { this.setColour(rawValue); } catch { @@ -1847,8 +1839,8 @@ export class Block { * @param json Structured data describing the block. * @param warningPrefix Warning prefix string identifying block. */ - private jsonInitStyle(json: AnyDuringMigration, warningPrefix: string) { - const blockStyleName = json['style']; + private jsonInitStyle(json: JsonBlockDefinition, warningPrefix: string) { + const blockStyleName = json.style! try { this.setStyle(blockStyleName); } catch { @@ -1901,7 +1893,7 @@ export class Block { */ private interpolate( message: string, - args: AnyDuringMigration[], + args: BlockArg[], implicitAlign: string | undefined, warningPrefix: string, ) { diff --git a/core/common.ts b/core/common.ts index 7f23779ec93..08882e9bad2 100644 --- a/core/common.ts +++ b/core/common.ts @@ -13,6 +13,7 @@ import type {Connection} from './connection.js'; import {EventType} from './events/type.js'; import * as eventUtils from './events/utils.js'; import {getFocusManager} from './focus_manager.js'; +import {JsonBlockDefinition} from './interfaces/i_json_block_definition.js'; import {ISelectable, isSelectable} from './interfaces/i_selectable.js'; import {ShortcutRegistry} from './shortcut_registry.js'; import type {Workspace} from './workspace.js'; @@ -238,7 +239,7 @@ export function getBlockTypeCounts( * @returns A function that calls jsonInit with the correct value * of jsonDef. */ -function jsonInitFactory(jsonDef: AnyDuringMigration): () => void { +function jsonInitFactory(jsonDef: JsonBlockDefinition): () => void { return function (this: Block) { this.jsonInit(jsonDef); }; @@ -250,14 +251,14 @@ function jsonInitFactory(jsonDef: AnyDuringMigration): () => void { * * @param jsonArray An array of JSON block definitions. */ -export function defineBlocksWithJsonArray(jsonArray: AnyDuringMigration[]) { +export function defineBlocksWithJsonArray(jsonArray: JsonBlockDefinition[]) { TEST_ONLY.defineBlocksWithJsonArrayInternal(jsonArray); } /** * Private version of defineBlocksWithJsonArray for stubbing in tests. */ -function defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) { +function defineBlocksWithJsonArrayInternal(jsonArray: JsonBlockDefinition[]) { defineBlocks(createBlockDefinitionsFromJsonArray(jsonArray)); } @@ -270,7 +271,7 @@ function defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) { * definitions created. */ export function createBlockDefinitionsFromJsonArray( - jsonArray: AnyDuringMigration[], + jsonArray: JsonBlockDefinition[], ): {[key: string]: BlockDefinition} { const blocks: {[key: string]: BlockDefinition} = {}; for (let i = 0; i < jsonArray.length; i++) { @@ -279,7 +280,7 @@ export function createBlockDefinitionsFromJsonArray( console.warn(`Block definition #${i} in JSON array is ${elem}. Skipping`); continue; } - const type = elem['type']; + const type = elem.type; if (!type) { console.warn( `Block definition #${i} in JSON array is missing a type attribute. ` + diff --git a/core/interfaces/i_json_block_definition.ts b/core/interfaces/i_json_block_definition.ts new file mode 100644 index 00000000000..e256ee37bbe --- /dev/null +++ b/core/interfaces/i_json_block_definition.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export interface JsonBlockDefinition { + type?: string; + style?: string; + colour?: string | number; + output: string | string[] | null; + previousStatement?: string | string[] | null; + nextStatement?: string | string[] | null; + outputShape?: number; + inputsInline?: boolean; + tooltip?: string; + helpUrl?: string; + extensions?: string[]; + mutator?: string; + enableContextMenu?: boolean; + suppressPrefixSuffix?: boolean; + + [key: `message${number}`]: string | undefined; + [key: `args${number}`]: BlockArg[] | undefined; + [key: `implicitAlign${number}`]: string | undefined; +} + +/** Block Arg */ +export type BlockArg = + | InputValueArg + | InputStatementArg + | InputDummyArg + | FieldInputArg + | FieldNumberArg + | FieldDropdownArg + | FieldCheckboxArg + | FieldImageArg + | FieldVariableArg; + +/** Common Arg */ +interface CommonArg { + name?: string; +} + +/** Input Args */ +interface InputValueArg extends CommonArg { + type: 'input_value'; + check?: string | string[]; + align?: FieldsAlign +} +interface InputStatementArg extends CommonArg { + type: 'input_statement'; + check?: string | string[]; +} +interface InputDummyArg extends CommonArg { + type: 'input_dummy'; +} + +/** Field Args */ +interface FieldInputArg extends CommonArg{ + type: 'field_input' + text: string +} + +interface FieldNumberArg extends CommonArg { + type: 'field_number'; + value?: number; + min?: number; + max?: number; + precision?: number; +} + +interface FieldDropdownArg extends CommonArg { + type: 'field_dropdown'; + options: [string, string][]; +} + +interface FieldCheckboxArg extends CommonArg { + type: 'field_checkbox'; + checked?: boolean | 'TRUE' | 'FALSE'; +} + +interface FieldImageArg { + type: 'field_image'; + src: string; + width: number; + height: number; + alt?: string; + flipRtl?: boolean | 'TRUE' | 'FALSE'; +} + +interface FieldVariableArg extends CommonArg { + type: 'field_variable' + variable: string | null +} + +export type FieldsAlign = 'LEFT' | 'RIGHT' | 'CENTRE'