Skip to content

Commit

Permalink
feat: add json definition generator
Browse files Browse the repository at this point in the history
  • Loading branch information
maribethb committed Feb 6, 2024
1 parent 635405c commit 112e45a
Show file tree
Hide file tree
Showing 16 changed files with 684 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export const connectionCheck = {
if (this.getField('CUSTOMCHECK')) {
return {customCheck: this.getFieldValue('CUSTOMCHECK')};
}
return {};
},
loadExtraState: function (state: ConnectionCheckState) {
this.customCheck = state?.customCheck;
Expand Down
16 changes: 15 additions & 1 deletion examples/developer-tools/src/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
connectionCheckGroup,
connectionCheckContainer,
connectionCheckItem,
} from './type';
} from './connection_check';
import {
fieldCheckbox,
fieldDropdown,
Expand All @@ -28,6 +28,20 @@ import {
} from './fields';
import {colourHue} from './colour';

// import for side effects for now
import '../output-generators/factory_base';
import '../output-generators/input';
import '../output-generators/connection_check';
import '../output-generators/fields/text_input';
import '../output-generators/fields/dropdown';
import '../output-generators/fields/number';
import '../output-generators/fields/label';
import '../output-generators/fields/label_serializable';
import '../output-generators/fields/checkbox';
import '../output-generators/fields/image';
import '../output-generators/fields/variable';
import '../output-generators/colour';

/* eslint-disable @typescript-eslint/naming-convention
-- Blockly convention is to use snake_case for block names
*/
Expand Down
20 changes: 20 additions & 0 deletions examples/developer-tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import * as Blockly from 'blockly/core';
import * as En from 'blockly/msg/en';
import {jsonDefinitionGenerator} from './output-generators/json_definition_generator';
import {registerAllBlocks} from './blocks';
import {save, load} from './serialization';
import {toolbox} from './toolbox';
Expand Down Expand Up @@ -37,6 +38,7 @@ mainWorkspace.addChangeListener(Blockly.Events.disableOrphans);
load(mainWorkspace);

// Whenever the workspace changes meaningfully, update the preview.
let blockName = '';
mainWorkspace.addChangeListener((e: Blockly.Events.Abstract) => {
// Don't run the code when the workspace finishes loading; we're
// already running it once when the application starts.
Expand All @@ -51,4 +53,22 @@ mainWorkspace.addChangeListener((e: Blockly.Events.Abstract) => {

save(mainWorkspace);
previewWorkspace.clear();

// If we previously had a block registered, delete it.
if (blockName) {
delete Blockly.Blocks[blockName];
}
const blockDefinitionString =
jsonDefinitionGenerator.workspaceToCode(mainWorkspace);
definitionDiv.textContent = blockDefinitionString;
const blockDefinition = JSON.parse(blockDefinitionString);
blockName = blockDefinition.type;
Blockly.common.defineBlocks(
Blockly.common.createBlockDefinitionsFromJsonArray([blockDefinition]),
);

// TODO: After Blockly v11, won't need to call `initSvg` and `render` directly.
const block = previewWorkspace.newBlock(blockName);
block.initSvg();
block.render();
});
29 changes: 29 additions & 0 deletions examples/developer-tools/src/output-generators/colour.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
JsonDefinitionGenerator,
Order,
jsonDefinitionGenerator,
} from './json_definition_generator';
import * as Blockly from 'blockly/core';

/**
* JSON definition for the "input" block.
*
* @param block
* @param generator
* @returns The stringified JSON representing the stack of input blocks.
* The entire stack will be returned due to the `scrub_` function.
* The JSON returned here should be part of the `args0` of the JSON
* in the final block definition.
*/
jsonDefinitionGenerator.forBlock['colour_hue'] = function (
block: Blockly.Block,
generator: JsonDefinitionGenerator,
): [string, number] {
return [this.getFieldValue('HUE').toString(), Order.ATOMIC];
};
81 changes: 81 additions & 0 deletions examples/developer-tools/src/output-generators/connection_check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import * as Blockly from 'blockly/core';
import {
JsonDefinitionGenerator,
Order,
jsonDefinitionGenerator,
} from './json_definition_generator';

/**
* JSON definition for a single "connection_check" block.
*
* @param block
* @param generator
* @returns A connection check string corresponding to the option chosen.
* If custom option is chosen and not specified, returns null.
*/
jsonDefinitionGenerator.forBlock['connection_check'] = function (
block: Blockly.Block,
generator: JsonDefinitionGenerator,
): [string, number] {
const selected = block.getFieldValue('CHECKDROPDOWN');
let output = '';
switch (selected) {
case 'null': {
output = null;
break;
}
case 'CUSTOM': {
output = block.getFieldValue('CUSTOMCHECK') || null;
break;
}
default: {
output = selected;
}
}

return [JSON.stringify(output), Order.ATOMIC];
};

/**
* JSON Definition for the "any of" check block.
*
* @param block
* @param generator
* @returns stringified version of:
* - null if the list is empty or contains the "any" check
* - a single check string if that is the only check present in the list
* - an array of all checks in the list, deduplicated
*/
jsonDefinitionGenerator.forBlock['connection_check_group'] = function (
block: Blockly.Block,
generator: JsonDefinitionGenerator,
): [string, number] {
const checks = new Set<string>();
for (const input of block.inputList) {
const value = generator.valueToCode(block, input.name, Order.ATOMIC);
if (!value) continue; // No block connected to this input
const check = JSON.parse(value) || '';
if (check === null) {
// If the list contains 'any' then the check is simplified to 'any'
return [JSON.stringify(null), Order.ATOMIC];
}
if (check) checks.add(check);
}

if (checks.size === 0) {
// No checks were connected to the block.
return [JSON.stringify(null), Order.ATOMIC];
}
if (checks.size === 1) {
// If there's only one check, we return it directly instead of
// returning an array with one member.
return [JSON.stringify(Array.from(checks)[0]), Order.ATOMIC];
}
return [JSON.stringify(Array.from(checks)), Order.ATOMIC];
};
125 changes: 125 additions & 0 deletions examples/developer-tools/src/output-generators/factory_base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import * as Blockly from 'blockly/core';
import {javascriptGenerator} from 'blockly/javascript';
import {
JsonDefinitionGenerator,
jsonDefinitionGenerator,
Order,
} from './json_definition_generator';

/**
* Builds the 'message0' part of the JSON block definition.
* The message should have label fields' text inlined into the message.
* Doing so makes the message more translatable as fields can be moved around.
*
* @param argsList The list of fields and inputs generated from the input stack.
* @returns An object containing:
* - a message string with one placeholder '%i`
* for each field and input in the block
* - the new args list, with label fields removed
*/
const buildMessageString = function (argsList: Array<Record<string, unknown>>) {
let i = 0;
let messageString = '';
const newArgs = [];
for (const arg of argsList) {
if (arg.type === 'field_label') {
// Label fields get added directly to the message string.
// They are removed from the arg list so they don't appear twice.
messageString += `${arg.text} `;
} else {
i++;
messageString += `%${i} `;
newArgs.push(arg);
}
}

return {
message: messageString.trim(),
args: newArgs,
};
};

jsonDefinitionGenerator.forBlock['factory_base'] = function (
block: Blockly.Block,
generator: JsonDefinitionGenerator,
): string {
// TODO: Get a JSON-legal name for the block
const blockName = block.getFieldValue('NAME');
// Tooltip and Helpurl string blocks can't be removed, so we don't care what happens if the block doesn't exist
const tooltip = JSON.parse(
generator.valueToCode(block, 'TOOLTIP', Order.ATOMIC),
);
const helpUrl = JSON.parse(
generator.valueToCode(block, 'HELPURL', Order.ATOMIC),
);

const code: {[key: string]: unknown} = {
type: blockName,
tooltip: tooltip,
helpUrl: helpUrl,
};

const inputsStack = generator.statementToCode(block, 'INPUTS');
if (inputsStack) {
// If there is a stack, they come back as the inner pieces of an array
// Can possibly fix this in scrub?
const args0 = JSON.parse(`[${inputsStack}]`);
const {args, message} = buildMessageString(args0);
code.message0 = message;
code.args0 = args;
} else {
code.message0 = '';
}

/**
* Sets the connection check for the given input, if present. If the input exists
* but doesn't have a block attached or a value, the connection property is
* still added to the output with a check of 'null'. If the input doesn't
* exist, that means the block should not have that type of connection, and no
* property is added to the output.
*
* @param inputName The name of the input that would contain the type check.
* @param connectionName The name of the connection in the definition output.
*/
const setConnectionChecks = (inputName: string, connectionName: string) => {
if (this.getInput(inputName)) {
// If there is no set type, we still need to add 'null` to the check
const output = generator.valueToCode(block, inputName, Order.ATOMIC);
code[connectionName] = output ? JSON.parse(output) : null;
}
};

setConnectionChecks('OUTPUTCHECK', 'output');
setConnectionChecks('TOPCHECK', 'previousStatement');
setConnectionChecks('BOTTOMCHECK', 'nextStatement');

const colour = generator.valueToCode(block, 'COLOUR', Order.ATOMIC);
if (colour !== '') {
code.colour = JSON.parse(colour);
}

const inputsAlign = block.getFieldValue('INLINE');
switch (inputsAlign) {
case 'EXT': {
code.inputsInline = false;
break;
}
case 'INT': {
code.inputsInline = true;
break;
}
default: {
// Don't add anything for "auto".
}
}

return JSON.stringify(code, undefined, 2);
};

jsonDefinitionGenerator.forBlock['text'] = javascriptGenerator.forBlock['text'];
23 changes: 23 additions & 0 deletions examples/developer-tools/src/output-generators/fields/checkbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import * as Blockly from 'blockly/core';
import {
JsonDefinitionGenerator,
jsonDefinitionGenerator,
} from '../json_definition_generator';

jsonDefinitionGenerator.forBlock['field_checkbox'] = function (
block: Blockly.Block,
generator: JsonDefinitionGenerator,
): string {
const code = {
type: 'field_checkbox',
name: block.getFieldValue('FIELDNAME'),
checked: block.getFieldValue('CHECKED'),
};
return JSON.stringify(code);
};
34 changes: 34 additions & 0 deletions examples/developer-tools/src/output-generators/fields/dropdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
JsonDefinitionGenerator,
jsonDefinitionGenerator,
} from '../json_definition_generator';
import {DropdownOptionData, FieldDropdownBlock} from '../../blocks/fields';

jsonDefinitionGenerator.forBlock['field_dropdown'] = function (
block: FieldDropdownBlock,
generator: JsonDefinitionGenerator,
): string {
const code: Record<string, string | Array<[DropdownOptionData, string]>> = {
type: 'field_dropdown',
name: block.getFieldValue('FIELDNAME'),
};
const options: Array<[DropdownOptionData, string]> = [];
for (let i = 0; i < block.optionList.length; i++) {
options.push([block.getUserData(i), block.getFieldValue('CPU' + i)]);
}

if (options.length === 0) {
// If there are no options in the dropdown, the field isn't valid.
// Remove it from the list of fields by returning an empty string.
return '';
}

code.options = options;
return JSON.stringify(code);
};
Loading

0 comments on commit 112e45a

Please sign in to comment.