Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions blocks/loops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_variable',
'name': 'VAR',
'variable': null,
},
{
'type': 'input_value',
Expand Down Expand Up @@ -167,7 +166,6 @@ export const blocks = createBlockDefinitionsFromJsonArray([
{
'type': 'field_variable',
'name': 'VAR',
'variable': null,
},
{
'type': 'input_value',
Expand Down
2 changes: 1 addition & 1 deletion blocks/variables_dynamic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export const blocks = createBlockDefinitionsFromJsonArray([
'variable': '%{BKY_VARIABLES_DEFAULT_NAME}',
},
],
Comment thread
heliacer marked this conversation as resolved.
'output': null,
'style': 'variable_dynamic_blocks',
'helpUrl': '%{BKY_VARIABLES_GET_HELPURL}',
'tooltip': '%{BKY_VARIABLES_GET_TOOLTIP}',
Expand All @@ -59,6 +58,7 @@ export const blocks = createBlockDefinitionsFromJsonArray([
'name': 'VALUE',
},
],
'output': null,
Comment thread
heliacer marked this conversation as resolved.
'previousStatement': null,
'nextStatement': null,
'style': 'variable_dynamic_blocks',
Expand Down
95 changes: 49 additions & 46 deletions core/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ import {StatementInput} from './inputs/statement_input.js';
import {ValueInput} from './inputs/value_input.js';
import {isCommentIcon} from './interfaces/i_comment_icon.js';
import {type IIcon} from './interfaces/i_icon.js';
import {
BlockArg,
JsonBlockDefinition,
} from './interfaces/i_json_block_definition.js';
import type {
IVariableModel,
IVariableState,
Expand Down Expand Up @@ -1709,20 +1713,20 @@ 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.',
);
}

// 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}).`,
Expand All @@ -1732,87 +1736,86 @@ 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;
// Handle legacy style object format for backwards compatibility
if (json.style && typeof json.style === 'object') {
this.hat = (json.style as {hat?: string}).hat;
// Must set to null so it doesn't error when checking for style and
// colour.
json['style'] = null;
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.');
Comment thread
heliacer marked this conversation as resolved.
Outdated
} else if (json['style']) {
} else if (json.style) {
this.jsonInitStyle(json, warningPrefix);
} else {
this.jsonInitColour(json, warningPrefix);
}

// 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] || [],
json[`message${i}`]!,
json[`args${i}`] || [],
// Backwards compatibility: lastDummyAlign aliases implicitAlign.
json['implicitAlign' + i] || json['lastDummyAlign' + i],
json[`implicitAlign${i}`] || (json as any)[`lastDummyAlign${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);
Expand All @@ -1826,12 +1829,12 @@ export class Block {
* @param json Structured data describing the block.
* @param warningPrefix Warning prefix string identifying block.
*/
private jsonInitColour(json: AnyDuringMigration, warningPrefix: string) {
private jsonInitColour(json: JsonBlockDefinition, warningPrefix: string) {
if ('colour' in json) {
if (json['colour'] === undefined) {
if (json.colour === undefined) {
console.warn(warningPrefix + 'Undefined colour value.');
} else {
const rawValue = json['colour'];
const rawValue = json.colour;
try {
this.setColour(rawValue);
} catch {
Expand All @@ -1847,8 +1850,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 {
Expand Down Expand Up @@ -1901,7 +1904,7 @@ export class Block {
*/
private interpolate(
message: string,
args: AnyDuringMigration[],
args: BlockArg[],
implicitAlign: string | undefined,
warningPrefix: string,
) {
Expand Down
11 changes: 6 additions & 5 deletions core/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 type {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';
Expand Down Expand Up @@ -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);
};
Expand All @@ -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));
}

Expand All @@ -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++) {
Expand All @@ -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. ` +
Expand Down
114 changes: 114 additions & 0 deletions core/interfaces/i_json_block_definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {FieldCheckboxFromJsonConfig} from '../field_checkbox.js';
import {FieldDropdownFromJsonConfig} from '../field_dropdown';
import {FieldImageFromJsonConfig} from '../field_image';
import {FieldNumberFromJsonConfig} from '../field_number';
import {FieldTextInputFromJsonConfig} from '../field_textinput';
import {FieldVariableFromJsonConfig} from '../field_variable';
Comment thread
heliacer marked this conversation as resolved.

/**
* Defines the JSON structure for a block definition in Blockly.
*
* @example
* ```typescript
* const blockDef: JsonBlockDefinition = {
* type: 'custom_block',
* message0: 'move %1 steps',
* args0: [
* {
* 'type': 'field_number',
* 'name': 'INPUT',
* },
* ],
* previousStatement: null,
* nextStatement: null,
* };
* ```
*/
export interface JsonBlockDefinition {
type: string;
style?: string | null;
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;
Comment thread
gonfunko marked this conversation as resolved.
[key: `args${number}`]: BlockArg[] | undefined;
[key: `implicitAlign${number}`]: string | undefined;
}
Comment thread
heliacer marked this conversation as resolved.

/** Block Arg */
export type BlockArg =
| InputValueArg
| InputStatementArg
| InputDummyArg
| FieldInputArg
| FieldNumberArg
| FieldDropdownArg
| FieldCheckboxArg
| FieldImageArg
| FieldVariableArg;

/** Input Args */
interface InputValueArg {
type: 'input_value';
name?: string;
check?: string | string[];
align?: FieldsAlign;
}
interface InputStatementArg {
type: 'input_statement';
name?: string;
check?: string | string[];
}
interface InputDummyArg {
type: 'input_dummy';
name?: string;
}
Comment thread
heliacer marked this conversation as resolved.

/** Field Args */
interface FieldInputArg extends FieldTextInputFromJsonConfig {
type: 'field_input';
name?: string;
}

interface FieldNumberArg extends FieldNumberFromJsonConfig {
type: 'field_number';
name?: string;
}

interface FieldDropdownArg extends FieldDropdownFromJsonConfig {
type: 'field_dropdown';
name?: string;
}

interface FieldCheckboxArg extends FieldCheckboxFromJsonConfig {
type: 'field_checkbox';
name?: string;
}

interface FieldImageArg extends FieldImageFromJsonConfig {
type: 'field_image';
name?: string;
}

interface FieldVariableArg extends FieldVariableFromJsonConfig {
type: 'field_variable';
name?: string;
}
Comment thread
heliacer marked this conversation as resolved.

export type FieldsAlign = 'LEFT' | 'RIGHT' | 'CENTRE';