Blockly applications often generate JavaScript as their output language, generally to run within a web page (possibly the same, or a embedded WebView). Like any generator, the first step is to include the JavaScript generator.
import {javascriptGenerator} from 'blockly/javascript';
To generate JavaScript from the workspace, call:
javascriptGenerator.addReservedWords('code');
var code = javascriptGenerator.workspaceToCode(workspace);
The resulting code can be executed right in the destination web page:
try {
eval(code);
} catch (e) {
alert(e);
}
Basically, the above snippet just generates the code and evals it. However,
there are a couple of refinements. One refinement is that the eval is wrapped in
a try
/catch
so that any runtime errors are visible, instead of failing
quietly. Another refinement is that code
is added to the list of reserved
words so that if the user's code contains a variable of that name it will be
automatically renamed instead of colliding. Any local variables should be
reserved in this way.
Highlight Blocks
Highlighting the currently executing block as the code runs helps users
understand their program's behaviour. Highlighting may be done on a
statement-by-statement level by setting STATEMENT_PREFIX
prior to
generating the JavaScript code:
javascriptGenerator.STATEMENT_PREFIX = 'highlightBlock(%1);\n';
javascriptGenerator.addReservedWords('highlightBlock');
Define highlightBlock
to mark the block on the workspace.
function highlightBlock(id) {
workspace.highlightBlock(id);
}
This results in the statement highlightBlock('123');
being added to before
every statement, where 123
is the serial number of the block to be
highlighted.
Infinite Loops
Although the resulting code is guaranteed to be syntactically correct at all
times, it may contain infinite loops. Since solving the
Halting problem is beyond
Blockly's scope (!) the best approach for dealing with these cases is to
maintain a counter and decrement it every time an iteration is performed.
To accomplish this, just set javascriptGenerator.INFINITE_LOOP_TRAP
to a code
snippet which will be inserted into every loop and every function. Here is an
example:
window.LoopTrap = 1000;
javascriptGenerator.INFINITE_LOOP_TRAP = 'if(--window.LoopTrap == 0) throw "Infinite loop.";\n';
var code = javascriptGenerator.workspaceToCode(workspace);
Example
Here is a live demo of generating and executing JavaScript.
JS-Interpreter
If you are serious about running the user's blocks properly, then the JS-Interpreter project is the way to go. This project is separate from Blockly, but was specifically written for Blockly.
- Execute code at any speed.
- Pause/resume/step-through execution.
- Highlight blocks as they execute.
- Completely isolated from browser's JavaScript.
Run the Interpreter
First, download the JS-Interpreter from GitHub:
Then add it to your page:
<script src="acorn_interpreter.js"></script>
The simplest method of calling it is to generate the JavaScript, create the interpreter, and run the code:
var code = javascriptGenerator.workspaceToCode(workspace);
var myInterpreter = new Interpreter(code);
myInterpreter.run();
Step the Interpreter
In order to execute the code slower, or in a more controlled manner, replace the
call to run
with a loop that steps (in this case one step every 10ms):
function nextStep() {
if (myInterpreter.step()) {
setTimeout(nextStep, 10);
}
}
nextStep();
Note that each step is not a line or a block, it is a semantic unit in JavaScript, which may be extremely fine-grained.
Add an API
The JS-Interpreter is a sandbox that is completely isolated from the browser. Any blocks that perform actions with the outside world require an API added to the interpreter. For a full description, see the JS-Interpreter documentation. But to start with, here is the API needed to support the alert and prompt blocks:
function initApi(interpreter, globalObject) {
// Add an API function for the alert() block.
var wrapper = function(text) {
return alert(arguments.length ? text : '');
};
interpreter.setProperty(globalObject, 'alert',
interpreter.createNativeFunction(wrapper));
// Add an API function for the prompt() block.
wrapper = function(text) {
return prompt(text);
};
interpreter.setProperty(globalObject, 'prompt',
interpreter.createNativeFunction(wrapper));
}
Then modify your interpreter initialization to pass in the initApi function:
var myInterpreter = new Interpreter(code, initApi);
The alert and prompt blocks are the only two blocks in the default set of blocks that require a custom API for the interpreter.
Connecting highlightBlock()
When running in JS-Interpreter, highlightBlock()
should be executed
immediately, outside the sandbox, as the user steps through the program. To do
this, create a wrapper function highlightBlock()
to capture the function
argument, and register it as a native function.
function initApi(interpreter, globalObject) {
// Add an API function for highlighting blocks.
var wrapper = function(id) {
return workspace.highlightBlock(id);
};
interpreter.setProperty(globalObject, 'highlightBlock',
interpreter.createNativeFunction(wrapper));
}
More sophisticated applications might wish to repeatedly execute steps without pause until a highlight command is reached, then pause. This strategy simulates line-by-line execution. The example below uses this approach.
JS-Interpreter Example
Here is a live demo of interpreting JavaScript step by step. And this demo includes a wait block, a good example to use for other asynchronous behavior (e.g., speech or audio, user input).