You will build a JSON generator that implements the JSON language spec.
In this codelab you will add code to the Blockly playground to create and use a new generator.
You will make all of your changes in a sample Blockly app, which you can find in blockly-samples at examples/sample-app
. This application contains a sample setup of Blockly, including custom blocks and a display of the generated code and output.
examples/sample-app
directory (or a copy of it) via the command line.npm install
to install the required dependencies.npm run start
to start the server and run the sample application.The initial application has one custom block and includes JavaScript generator definitions for that block. Since we will be creating a JSON generator, we'll remove that custom block and add our own.
You can view the complete code used in this codelab in blockly-samples under examples/custom-generator-codelab
.
Before setting up the rest of the application, let's change the storage key used for this codelab application. This will ensure that the workspace is saved in its own storage, separate from the regular sample app, so that we don't interfere with other demos. In serialization.js
, change the value of storageKey
to some unique string:
// Use a unique storage key for this codelab
const storageKey = 'jsonGeneratorWorkspace';
For this codelab you will use two custom blocks, as well as five blocks from Blockly's standard set.
The custom blocks represent the Object and Member sections of the JSON specification.
The blocks are:
object
member
math_number
text
logic_boolean
logic_null
lists_create_with
Create a new file in the src/blocks/
directory called json.js
. This will hold our custom JSON-related blocks. Add the following code:
import * as Blockly from 'blockly';
export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([{
"type": "object",
"message0": "{ %1 %2 }",
"args0": [
{
"type": "input_dummy"
},
{
"type": "input_statement",
"name": "MEMBERS"
}
],
"output": null,
"colour": 230,
},
{
"type": "member",
"message0": "%1 %2 %3",
"args0": [
{
"type": "field_input",
"name": "MEMBER_NAME",
"text": ""
},
{
"type": "field_label",
"name": "COLON",
"text": ":"
},
{
"type": "input_value",
"name": "MEMBER_VALUE"
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 230,
}]);
This code creates the block definitions, but it doesn't register the definitions with Blockly to make the blocks usable. We'll do that in src/index.js
. Currently, we import blocks
from the original sample file, text.js
. Instead, we want to import the definitions we just added. Remove the original import:
// Remove this!
import {blocks} from './blocks/text';
and add the import for the new blocks:
import {blocks} from './blocks/json';
Later in this file, we register the block definitions with Blockly (this code is already present and you do not need to add it):
Blockly.common.defineBlocks(blocks);
Next, we need to define a toolbox that includes these custom blocks. For this example we have a flyout-only toolbox with seven blocks in it.
The file src/toolbox.js
contains the original sample toolbox. Replace the entire contents of that file with this code:
export const toolbox = {
'kind': 'flyoutToolbox',
'contents': [
{
'kind': 'block',
'type': 'object'
},
{
'kind': 'block',
'type': 'member'
},
{
'kind': 'block',
'type': 'math_number'
},
{
'kind': 'block',
'type': 'text'
},
{
'kind': 'block',
'type': 'logic_boolean'
},
{
'kind': 'block',
'type': 'logic_null'
},
{
'kind': 'block',
'type': 'lists_create_with'
},
]
}
Our index.js
file already handles importing the toolbox and using it in Blockly.
If the server is already running, you can refresh the page to see your changes. Otherwise, run npm run start
to start the server. You should see the new blocks in the toolbox, like this:
The app is still trying to generate and run JavaScript for the workspace, instead of JSON. We will change that soon.
A language generator defines basic properties of your language, such as how indentation works. Block generators define how individual blocks are turned into code, and must be defined for every block you use.
A language generator has a single entry point: workspaceToCode
. This function takes in a workspace and:
init
.blockToCode
on each top block.finish
.The first step is to define and call your language generator.
A custom language generator is simply an instance of Blockly.Generator
. Create a new file src/generators/json.js
. In it, import Blockly, call the Blockly.Generator
constructor, passing in your generator's name, and store the result.
import * as Blockly from 'blockly';
export const jsonGenerator = new Blockly.Generator('JSON');
Next, let's hook up the new generator with the sample app. First, we'll remove the old code that imports the new block generator properties and assigns them to the javascriptGenerator
. Remove these lines from src/index.js
:
// Remove these lines!
import {generator} from './generators/javascript';
import {javascriptGenerator} from 'blockly/javascript';
Object.assign(javascriptGenerator, generator);
Then, we need to import the new generator:
import {jsonGenerator} from './generators/json';
Next, we need to change the output of the sample app. Currently, there are two panels in the app next to the workspace. One shows the generated JavaScript code, and one executes it. We need to change it to show the generated JSON code instead of JavaScript. And since we can't execute JSON, we will leave the bottom panel blank and not show anything there. Change the runCode
function to match the following:
const runCode = () => {
const code = jsonGenerator.workspaceToCode(ws);
codeDiv.innerText = code;
};
After we define the block generators, we'll automatically show the generated code in the top left panel. Refresh the sample app page to see your changes so far.
Put a number block on the workspace and check the generator output area. It's empty, so let's check the console. You should see an error:
Language "JSON" does not know how to generate code for block type "math_number".
This error occurs because you need to write a block generator for each type of block. Read the next section for more details.
At its core, a block generator is a function that takes in a block, translates the block into code, and returns that code as a string.
Block generators are defined on the language generator object. For instance, here is the code to add a block generator for blocks of type sample_block
on a language generator object (sampleGenerator
).
sampleGenerator['sample_block'] = function(block) {
return 'my code string';
}
Statement blocks represent code that does not return a value.
A statement block's generator simply returns a string.
For example, this code defines a block generator that always returns the same function call.
sampleGenerator['left_turn_block'] = function(block) {
return 'turnLeft()';
}
Value blocks represent code that returns a value.
A value block's generator returns an array containing a string and a precedence value.
For example, this code defines a block generator that always returns 1 + 1
:
sampleGenerator['two_block'] = function(block) {
return ['1 + 1', sampleGenerator.ORDER_ADDITION];
}
Operator precedence rules determine how the correct order of operations is maintained during parsing. In Blockly's generators, operator precedence determines when to add parentheses.
–> Read more about operator precedence in JavaScript.
–> Read more about operator precedence in Blockly.
Since JSON does not allow values that are expressions, you do not need to consider operator precedence for the generator that you are building in this codelab. You can use the same value everywhere a precedence value is required. In this case, we'll call it PRECEDENCE
.
You need to be able to access this value inside your block generators, so add PRECEDENCE
to your language generator:
jsonGenerator.PRECEDENCE = 0;
In this step you will build the generators for the simple value blocks: logic_null
, text
, math_number
, and logic_boolean
.
You will use getFieldValue
on several types of fields.
The simplest block in this example is the logic_null
block.
No matter what, it generates the code 'null'
. Notice that this is a string, because all generated code is a string.
jsonGenerator['logic_null'] = function(block) {
return ['null', jsonGenerator.PRECEDENCE];
};
Next is the text
block.
Unlike logic_null
, there is a single text input field on this block. Use getFieldValue
:
const textValue = block.getFieldValue('TEXT');
Since this is a string in the generated code, wrap the value in quotation marks and return it:
jsonGenerator['text'] = function(block) {
const textValue = block.getFieldValue('TEXT');
const code = `"${textValue}"`;
return [code, jsonGenerator.PRECEDENCE];
};
The math_number
block has a number field.
Like the text
block, you can use getFieldValue
. Unlike the text block, you don't need to wrap it in additional quotation marks, because in the JSON code, it won't be a string.
However, like all generated code and as with null
above, we need to return the code as a string from the generator.
jsonGenerator['math_number'] = function(block) {
const code = String(block.getFieldValue('NUM'));
return [code, jsonGenerator.PRECEDENCE];
};
The logic_boolean
block has a dropdown field named BOOL
.
Calling getFieldValue
on a dropdown field returns the value of the selected option, which may not be the same as the display text. In this case the dropdown has two possible values: TRUE
and FALSE
.
jsonGenerator['logic_boolean'] = function(block) {
const code = (block.getFieldValue('BOOL') === 'TRUE') ? 'true' : 'false';
return [code, jsonGenerator.PRECEDENCE];
};
getFieldValue
finds the field with the specified name and returns its value.getFieldValue
depends on the type of the field. In this step you will build the generator for the member
block. You will use getFieldValue
, and add valueToCode
to your tool kit.
The member block has a text input field and a value input.
The generated code looks like "property name": "property value"
.
The property name is the value of the text input, which we get with getFieldValue
:
const name = block.getFieldValue('MEMBER_NAME');
Remember that in src/blocks/json.js
we defined this block to have a text input field called MEMBER_NAME
- that's the field whose value we're getting here.
The property value is whatever is attached to the value input. A variety of blocks could be attached there: logic_null
, text
, math_number
, logic_boolean
, or even an array (lists_create_with
). Use valueToCode
to get the correct value:
const value = jsonGenerator.valueToCode(block, 'MEMBER_VALUE',
jsonGenerator.PRECEDENCE);
valueToCode
does three things:
If no block is attached, valueToCode
returns null
. In another generator you might need to replace null
with a different default value; in JSON, null
is fine.
The third argument is related to operator precedence. It is used to determine if parentheses need to be added around the value. In JSON, parentheses will never be added, as discussed in an earlier section.
Next, assemble the arguments name
and value
into the correct code, of the form "name": value
.
const code = `"${name}": ${value}`;
All together, here is block generator for the member block:
jsonGenerator['member'] = function(block) {
const name = block.getFieldValue('MEMBER_NAME');
const value = jsonGenerator.valueToCode(
block, 'MEMBER_VALUE', jsonGenerator.PRECEDENCE);
const code = `"${name}": ${value}`;
return code;
};
In this step you will build the generator for the array block. You will learn how to indent code and handle a variable number of inputs.
The array block uses a mutator to dynamically change the number of inputs it has.
The generated code looks like:
[
1,
"two",
false,
true
]
As with member blocks, there are no restrictions on the types of blocks connected to inputs.
Each value input on the block has a name: ADD0
, ADD1
, etc. Use valueToCode
in a loop to build an array of values:
const values = [];
for (let i = 0; i < block.itemCount_; i++) {
const valueCode = jsonGenerator.valueToCode(block, 'ADD' + i,
jsonGenerator.PRECEDENCE);
if (valueCode) {
values.push(valueCode);
}
}
Notice that we skip empty inputs by checking if valueCode
is null
.
If you want to include empty inputs, use the string 'null'
as the value.
const values = [];
for (let i = 0; i < block.itemCount_; i++) {
const valueCode = jsonGenerator.valueToCode(block, 'ADD' + i,
jsonGenerator.PRECEDENCE) || 'null';
values.push(valueCode);
}
At this point values
is an array of string
s. The strings contain the generated code for each input.
Convert the list into a single string
, with a comma and newline separating each element:
const valueString = values.join(',\n');
Next, use prefixLines
to add indentation at the beginning of each line:
const indentedValueString =
jsonGenerator.prefixLines(valueString, jsonGenerator.INDENT);
INDENT
is a property on the generator. It defaults to two spaces, but language generators may override it to increase indent or change to tabs.
Finally, wrap the indented values in brackets and return the string:
const codeString = '[\n' + indentedValueString + '\n]';
return [codeString, jsonGenerator.PRECEDENCE];
Here is the final array block generator:
jsonGenerator['lists_create_with'] = function(block) {
const values = [];
for (let i = 0; i < block.itemCount_; i++) {
const valueCode = jsonGenerator.valueToCode(block, 'ADD' + i,
jsonGenerator.PRECEDENCE);
if (valueCode) {
values.push(valueCode);
}
}
const valueString = values.join(',\n');
const indentedValueString =
jsonGenerator.prefixLines(valueString, jsonGenerator.INDENT);
const codeString = '[\n' + indentedValueString + '\n]';
return [codeString, jsonGenerator.PRECEDENCE];
};
Test the block generator by adding an array to your onscreen blocks and populating it.
What code does it generate if you have no inputs?
What if you have five inputs, one of which is empty?
In this section you will write the generator for the object
block. You will learn how to use statementToCode
.
The object
block generates code for a JSON Object. It has a single statement input, in which member blocks may be stacked.
The generated code looks like this:
{
"a": true,
"b": "one",
"c": 1
}
We'll use statementToCode
to get the code for the blocks attached to the statement input of our object
block.
statementToCode
does three things:
In this case the input name is 'MEMBERS'
.
const statement_members =
jsonGenerator.statementToCode(block, 'MEMBERS');
Wrap the statements in curly brackets and return the code, using the default precedence:
const code = '{\n' + statement_members + '\n}';
return [code, jsonGenerator.PRECEDENCE];
Note that statementToCode
handles the indentation automatically.
Here is the full block generator:
jsonGenerator['object'] = function(block) {
const statementMembers =
jsonGenerator.statementToCode(block, 'MEMBERS');
const code = '{\n' + statementMembers + '\n}';
return [code, jsonGenerator.PRECEDENCE];
};
Test it by generating code for an object
block containing a single member
block. The result should look like this:
{
"test": true
}
Next, add a second member block and rerun the generator. Did the resulting code change? Let's look at the next section to find out why not.
The scrub_
function is called on every block from blockToCode
. It takes in three arguments:
block
is the current block.code
is the code generated for this block, which includes code from all attached value blocks.opt_thisOnly
is an optional boolean
. If true, code should be generated for this block but no subsequent blocks.By default, scrub_
simply returns the passed-in code. A common pattern is to override the function to also generate code for any blocks that follow the current block in a stack. In this case, we will add commas and newlines between object members:
jsonGenerator.scrub_ = function(block, code, thisOnly) {
const nextBlock =
block.nextConnection && block.nextConnection.targetBlock();
if (nextBlock && !thisOnly) {
return code + ',\n' + jsonGenerator.blockToCode(nextBlock);
}
return code;
};
Create a stack of member
blocks on the workspace. You should see generated code for all of your blocks, not just the first one.
Next, add an object
block and drag your member
blocks into it. This case tests statementToCode
, and should generate code for all of your blocks.
In this codelab you:
statementToCode
, valueToCode
, blockToCode
, and getFieldValue
.JSON is a simple language, and there are many additional features you may want to implement in your generator. Blockly's built-in language generators are a good place to learn more about some additional features:
Blockly ships with five language generators: Python, Dart, JavaScript, PHP, and Lua. You can find the language generators and block generators in the generators directory.